Skip to content

Commit

Permalink
#74 Add environment to approvals (#52)
Browse files Browse the repository at this point in the history
* Add environment flag to reportApproval command

* Approval: only send newest commit in commit list. Added sending oldest commit in seperate parameter

* parse the input oldest and newset commit to verify they exist and get their sha1 hashes

* Commands: add environment flag for request approval.

* Updated approval documentation and CLI reference

* Get oldest commit from kosli server if not given in command line

* Remove newest_commit key from approval payload

* Add test cases and fix typos in documentation

* Approval: Updated URL. Started on some more tests

* Create setup to make a snapshot in approval tests

* Approval: calculate HEAD~5 in kosli approval command

* Tests: calculate fingerprint of artifact for approval using function instead of kosli command

* Delete dead comments

* Update docs for approvals

* Docs: fix typos

* Approval: Small update docs

---------

Co-authored-by: Tore Martin Hagen <[email protected]>
Co-authored-by: Sami Alajrami <[email protected]>
Co-authored-by: Arstanaly Rysbekov <[email protected]>
  • Loading branch information
4 people authored Nov 29, 2023
1 parent 562ff7a commit 98ee5c6
Show file tree
Hide file tree
Showing 10 changed files with 335 additions and 173 deletions.
4 changes: 2 additions & 2 deletions charts/k8s-reporter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ helm upgrade [RELEASE-NAME] kosli/k8s-reporter
| kosliApiToken.secretKey | string | `""` | the name of the key in the secret data which contains the kosli API token |
| kosliApiToken.secretName | string | `""` | the name of the secret containing the kosli API token |
| nameOverride | string | `""` | overrides the name used for the created k8s resources. If `fullnameOverride` is provided, it has higher precedence than this one |
| podAnnotations | object | `{}` | |
| podAnnotations | object | `{}` | any custom annotations to be added to the cronjob |
| reporterConfig.dryRun | bool | `false` | whether the dry run mode is enabled or not. In dry run mode, the reporter logs the reports to stdout and does not send them to kosli. |
| reporterConfig.kosliEnvironmentName | string | `""` | the name of kosli environment that the k8s cluster/namespace correlates to |
| reporterConfig.kosliOrg | string | `""` | the name of the kosli org |
Expand All @@ -74,5 +74,5 @@ helm upgrade [RELEASE-NAME] kosli/k8s-reporter
| serviceAccount.name | string | `""` | the name of the service account to use. If not set and create is true, a name is generated using the fullname template |

----------------------------------------------
Autogenerated from chart metadata using [helm-docs v1.5.0](https://github.com/norwoodj/helm-docs/releases/v1.5.0)
Autogenerated from chart metadata using [helm-docs v1.11.0](https://github.com/norwoodj/helm-docs/releases/v1.11.0)

14 changes: 14 additions & 0 deletions cmd/kosli/cli_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,20 @@ func MuXRequiredFlags(cmd *cobra.Command, flagNames []string, atLeastOne bool) e
return nil
}

func RequireAtLeastOneOfFlags(cmd *cobra.Command, flagNames []string) error {
flagsSet := 0
for _, name := range flagNames {
flag := cmd.Flags().Lookup(name)
if flag.Changed {
flagsSet++
}
}
if flagsSet == 0 {
return fmt.Errorf("at least one of %s is required", JoinFlagNames(flagNames))
}
return nil
}

// JoinFlagNames returns a comma-separated string of flag names with "--" prefix
// from a list of plain names
func JoinFlagNames(flagNames []string) string {
Expand Down
105 changes: 94 additions & 11 deletions cmd/kosli/reportApproval.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"encoding/json"
"fmt"
"io"
"net/http"
Expand All @@ -11,30 +12,45 @@ import (
"github.com/spf13/cobra"
)

const reportApprovalShortDesc = `Report an approval of deploying an artifact to Kosli. `
const reportApprovalLongDesc = reportApprovalShortDesc + `
const (
reportApprovalShortDesc = `Report an approval of deploying an artifact to an environment to Kosli. `
reportApprovalLongDesc = reportApprovalShortDesc + `
` + fingerprintDesc
)

const reportApprovalExample = `
# Report that a file type artifact has been approved for deployment.
# The approval is for the last 5 git commits
# Report that an artifact with a provided fingerprint (sha256) has been approved for
# deployment to environment <yourEnvironmentName>.
# The approval is for all git commits since the last approval to this environment.
kosli report approval \
--api-token yourAPIToken \
--description "An optional description for the approval" \
--environment yourEnvironmentName \
--approver username \
--org yourOrgName \
--flow yourFlowName \
--fingerprint yourArtifactFingerprint
# Report that a file type artifact has been approved for deployment to environment <yourEnvironmentName>.
# The approval is for all git commits since the last approval to this environment.
kosli report approval FILE.tgz \
--api-token yourAPIToken \
--artifact-type file \
--description "An optional description for the approval" \
--newest-commit $(git rev-parse HEAD) \
--oldest-commit $(git rev-parse HEAD~5) \
--environment yourEnvironmentName \
--newest-commit HEAD \
--approver username \
--org yourOrgName \
--flow yourFlowName
# Report that an artifact with a provided fingerprint (sha256) has been approved for deployment.
# The approval is for the last 5 git commits
# The approval is for all environments.
# The approval is for all commits since the git commit of origin/production branch.
kosli report approval \
--api-token yourAPIToken \
--description "An optional description for the approval" \
--newest-commit $(git rev-parse HEAD) \
--oldest-commit $(git rev-parse HEAD~5) \
--newest-commit HEAD \
--oldest-commit origin/production \
--approver username \
--org yourOrgName \
--flow yourFlowName \
Expand All @@ -54,8 +70,10 @@ type reportApprovalOptions struct {

type ApprovalPayload struct {
ArtifactFingerprint string `json:"artifact_fingerprint"`
Environment string `json:"environment,omitempty"`
Description string `json:"description"`
CommitList []string `json:"src_commit_list"`
OldestCommit string `json:"oldest_commit,omitempty"`
Reviews []map[string]string `json:"approvals"`
UserData interface{} `json:"user_data"`
}
Expand All @@ -74,6 +92,11 @@ func newReportApprovalCmd(out io.Writer) *cobra.Command {
return ErrorBeforePrintingUsage(cmd, err.Error())
}

err = RequireAtLeastOneOfFlags(cmd, []string{"environment", "oldest-commit"})
if err != nil {
return err
}

err = ValidateArtifactArg(args, o.fingerprintOptions.artifactType, o.payload.ArtifactFingerprint, false)
if err != nil {
return ErrorBeforePrintingUsage(cmd, err.Error())
Expand All @@ -86,6 +109,7 @@ func newReportApprovalCmd(out io.Writer) *cobra.Command {
}

cmd.Flags().StringVarP(&o.payload.ArtifactFingerprint, "fingerprint", "F", "", fingerprintFlag)
cmd.Flags().StringVarP(&o.payload.Environment, "environment", "e", "", approvalEnvironmentNameFlag)
cmd.Flags().StringVarP(&o.flowName, "flow", "f", "", flowNameFlag)
cmd.Flags().StringVarP(&o.payload.Description, "description", "d", "", approvalDescriptionFlag)
cmd.Flags().StringVarP(&o.userDataFile, "user-data", "u", "", approvalUserDataFlag)
Expand All @@ -96,7 +120,7 @@ func newReportApprovalCmd(out io.Writer) *cobra.Command {
addFingerprintFlags(cmd, o.fingerprintOptions)
addDryRunFlag(cmd)

err := RequireFlags(cmd, []string{"flow", "oldest-commit"})
err := RequireFlags(cmd, []string{"flow"})
if err != nil {
logger.Error("failed to configure required flags: %v", err)
}
Expand All @@ -117,12 +141,71 @@ func (o *reportApprovalOptions) run(args []string, request bool) error {
if err != nil {
return err
}
gitView, err := gitview.New(o.srcRepoRoot)
if err != nil {
return err
}

o.payload.CommitList, err = o.payloadCommitList()
o.newestSrcCommit, err = gitView.ResolveRevision(o.newestSrcCommit)
if err != nil {
return err
}

if o.oldestSrcCommit != "" {
o.payload.OldestCommit, err = gitView.ResolveRevision(o.oldestSrcCommit)
if err != nil {
return err
}
o.payload.CommitList, err = o.payloadCommitList()
if err != nil {
return err
}
} else {
// Request last approved git commit from kosli server
url := fmt.Sprintf("%s/api/v2/approvals/%s/%s/artifact-commit/%s", global.Host, global.Org,
o.flowName, o.payload.Environment)

getLastApprovedGitCommitParams := &requests.RequestParams{
Method: http.MethodGet,
URL: url,
DryRun: false,
Password: global.ApiToken,
}

lastApprovedGitCommitResponse, err := kosliClient.Do(getLastApprovedGitCommitParams)

if err != nil {
if !global.DryRun {
// error and not dry run -> print error message and return err
return err
} else {
// error and dry run -> set src_commit_list to o.newestCommit do not send oldestCommit
o.payload.CommitList = []string{o.newestSrcCommit}
}
} else {
var responseData map[string]interface{}
err = json.Unmarshal([]byte(lastApprovedGitCommitResponse.Body), &responseData)
if err != nil {
fmt.Println("unmarshal failed")
return err
}

if responseData["commit_sha"] != nil {
// no error we get back a git commit -> call o.payloadCommitList()
o.oldestSrcCommit = responseData["commit_sha"].(string)
o.payload.OldestCommit = o.oldestSrcCommit
o.payload.CommitList, err = o.payloadCommitList()
if err != nil {
return err
}
} else {
// no error we get back None -> set src_commit_list to o.newestCommit do not send oldestCommit
o.payload.CommitList = []string{o.newestSrcCommit}
}

}
}

url := fmt.Sprintf("%s/api/v2/approvals/%s/%s", global.Host, global.Org, o.flowName)

reqParams := &requests.RequestParams{
Expand Down
59 changes: 55 additions & 4 deletions cmd/kosli/reportApproval_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import (
"fmt"
"testing"

"github.com/kosli-dev/cli/internal/digest"
"github.com/kosli-dev/cli/internal/gitview"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)

Expand All @@ -12,6 +15,13 @@ type ApprovalReportTestSuite struct {
defaultKosliArguments string
artifactFingerprint string
flowName string
envName string
gitCommit string
artifactPath string
}

type reportApprovalTestConfig struct {
createSnapshot bool
}

func (suite *ApprovalReportTestSuite) SetupTest() {
Expand All @@ -22,11 +32,25 @@ func (suite *ApprovalReportTestSuite) SetupTest() {
}

suite.defaultKosliArguments = fmt.Sprintf(" --host %s --org %s --api-token %s", global.Host, global.Org, global.ApiToken)
suite.artifactFingerprint = "847411c6124e719a4e8da2550ac5c116b7ff930493ce8a061486b48db8a5aaa0"
suite.flowName = "approval-test"
suite.envName = "staging"
t := suite.T()

gitView, err := gitview.New("../..")
require.NoError(t, err, "Failed to create gitview")

suite.gitCommit, err = gitView.ResolveRevision("HEAD~5")
require.NoError(t, err, "Failed to get HEAD~5")

CreateFlow(suite.flowName, suite.T())
CreateArtifact(suite.flowName, suite.artifactFingerprint, "foobar", suite.T())
suite.artifactPath = "testdata/report.xml"
// We cannot get the digest of the file by running the 'kosli fingerprint' command
// by using executeCommandC() because this function overwrites the global options
suite.artifactFingerprint, err = digest.FileSha256(suite.artifactPath)
require.NoError(t, err, "Failed to calculate fingerprint")

CreateFlow(suite.flowName, t)
CreateArtifactWithCommit(suite.flowName, suite.artifactFingerprint, suite.artifactPath, suite.gitCommit, t)
CreateEnv(global.Org, suite.envName, "server", t)
}

func (suite *ApprovalReportTestSuite) TestApprovalReportCmd() {
Expand All @@ -37,8 +61,35 @@ func (suite *ApprovalReportTestSuite) TestApprovalReportCmd() {
--newest-commit HEAD --oldest-commit HEAD~3` + suite.defaultKosliArguments,
golden: fmt.Sprintf("approval created for artifact: %s\n", suite.artifactFingerprint),
},
{
name: "report approval with an environment name works",
cmd: `report approval --fingerprint ` + suite.artifactFingerprint + ` --flow ` + suite.flowName + ` --repo-root ../..
--newest-commit HEAD --oldest-commit HEAD~3` + ` --environment staging` + suite.defaultKosliArguments,
golden: fmt.Sprintf("approval created for artifact: %s\n", suite.artifactFingerprint),
},
{
wantError: true,
name: "report approval with no environment name and no oldest commit fails",
cmd: `report approval --fingerprint ` + suite.artifactFingerprint + ` --flow ` + suite.flowName + ` --repo-root ../.. ` +
suite.defaultKosliArguments,
golden: "Error: at least one of --environment, --oldest-commit is required\n",
},
{
name: "report approval with an environment name and no oldest-commit and no newest-commit works",
cmd: `report approval --fingerprint ` + suite.artifactFingerprint + ` --flow ` + suite.flowName + ` --repo-root ../.. ` +
` --environment ` + suite.envName + suite.defaultKosliArguments,
golden: fmt.Sprintf("approval created for artifact: %s\n", suite.artifactFingerprint),
additionalConfig: reportApprovalTestConfig{
createSnapshot: true,
},
},
}
for _, t := range tests {
if t.additionalConfig != nil && t.additionalConfig.(reportApprovalTestConfig).createSnapshot {
ReportServerArtifactToEnv([]string{suite.artifactPath}, suite.envName, suite.T())
}
runTestCmd(suite.T(), []cmdTestCase{t})
}
runTestCmd(suite.T(), tests)
}

func TestApprovalReportCommandTestSuite(t *testing.T) {
Expand Down
44 changes: 32 additions & 12 deletions cmd/kosli/requestApproval.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,47 @@ import (
"github.com/spf13/cobra"
)

const requestApprovalShortDesc = `Request an approval of a deployment of an artifact in Kosli. `
const requestApprovalLongDesc = requestApprovalShortDesc + `
const (
requestApprovalShortDesc = `Request an approval of a deployment of an artifact to an environment in Kosli. `
requestApprovalLongDesc = requestApprovalShortDesc + `
The request should be reviewed in the Kosli UI.
` + fingerprintDesc
)

const requestApprovalExample = `
# Request that a file type artifact needs approval.
# The approval is for the last 5 git commits
# Request an approval for an artifact with a provided fingerprint (sha256)
# for deployment to environment <yourEnvironmentName>.
# The approval is for all git commits since the last approval to this environment.
kosli request approval \
--api-token yourAPIToken \
--description "An optional description for the approval" \
--environment yourEnvironmentName \
--org yourOrgName \
--flow yourFlowName \
--fingerprint yourArtifactFingerprint
# Request that a file type artifact needs approval for deployment to environment <yourEnvironmentName>.
# The approval is for all git commits since the last approval to this environment.
kosli request approval FILE.tgz \
--api-token yourAPIToken \
--artifact-type file \
--description "An optional description for the requested approval" \
--newest-commit $(git rev-parse HEAD) \
--oldest-commit $(git rev-parse HEAD~5) \
--environment yourEnvironmentName \
--newest-commit HEAD \
--org yourOrgName \
--flow yourFlowName
# Request and approval for an artifact with a provided fingerprint (sha256).
# The approval is for the last 5 git commits
# Request an approval for an artifact with a provided fingerprint (sha256).
# The approval is for all environments.
# The approval is for all commits since the git commit of origin/production branch.
kosli request approval \
--api-token yourAPIToken \
--description "An optional description for the requested approval" \
--newest-commit $(git rev-parse HEAD) \
--oldest-commit $(git rev-parse HEAD~5) \
--newest-commit HEAD \
--oldest-commit origin/production \
--org yourOrgName \
--flow yourFlowName \
--fingerprint yourArtifactFingerprint
--fingerprint yourArtifactFingerprint
`

func newRequestApprovalCmd(out io.Writer) *cobra.Command {
Expand All @@ -49,6 +63,11 @@ func newRequestApprovalCmd(out io.Writer) *cobra.Command {
return ErrorBeforePrintingUsage(cmd, err.Error())
}

err = RequireAtLeastOneOfFlags(cmd, []string{"environment", "oldest-commit"})
if err != nil {
return err
}

err = ValidateArtifactArg(args, o.fingerprintOptions.artifactType, o.payload.ArtifactFingerprint, false)
if err != nil {
return ErrorBeforePrintingUsage(cmd, err.Error())
Expand All @@ -61,6 +80,7 @@ func newRequestApprovalCmd(out io.Writer) *cobra.Command {
}

cmd.Flags().StringVarP(&o.payload.ArtifactFingerprint, "fingerprint", "F", "", fingerprintFlag)
cmd.Flags().StringVarP(&o.payload.Environment, "environment", "e", "", approvalEnvironmentNameFlag)
cmd.Flags().StringVarP(&o.flowName, "flow", "f", "", flowNameFlag)
cmd.Flags().StringVarP(&o.payload.Description, "description", "d", "", approvalDescriptionFlag)
cmd.Flags().StringVarP(&o.userDataFile, "user-data", "u", "", approvalUserDataFlag)
Expand All @@ -70,7 +90,7 @@ func newRequestApprovalCmd(out io.Writer) *cobra.Command {
addFingerprintFlags(cmd, o.fingerprintOptions)
addDryRunFlag(cmd)

err := RequireFlags(cmd, []string{"flow", "oldest-commit"})
err := RequireFlags(cmd, []string{"flow"})
if err != nil {
logger.Error("failed to configure required flags: %v", err)
}
Expand Down
Loading

0 comments on commit 98ee5c6

Please sign in to comment.