Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#74 Add environment to approvals #52

Merged
merged 17 commits into from
Nov 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading