Skip to content

Commit

Permalink
feat: add deploy trigger and deploy add status commands (#215)
Browse files Browse the repository at this point in the history
  • Loading branch information
davidebianchi authored Jul 26, 2024
1 parent 4805132 commit 87efd8f
Show file tree
Hide file tree
Showing 9 changed files with 509 additions and 133 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- `deploy add status` command
- `deploy trigger` command

### Changed

- deprecate `deploy` command

## [v0.14.0] - 2024-07-25

### Added
Expand Down
36 changes: 35 additions & 1 deletion docs/30_commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -542,12 +542,23 @@ Available flags for the command:

## deploy

The `deploy` command allows you to manage the deployment of your Projects.

Available subcommands are the following ones:

```sh
trigger Trigger a deploy pipeline
add status Add a new deploy status
```

### trigger

This command allows you to trigger the deploy pipeline for the selected Project.

Usage:

```sh
miactl deploy ENVIRONMENT [flags]
miactl deploy trigger ENVIRONMENT [flags]
```

Available flags for the command:
Expand All @@ -562,6 +573,29 @@ Available flags for the command:
- `--no-semver`, to force the deploy without `semver`
- `--revision`, to specify the revision of the commit to deploy

### add status

This command allows you to add a new deploy status for the selected trigger id pipelines of the Project,
only for those integration which trigger the pipeline with a trigger id (e.g. Jenkins integration).

Usage:

```sh
miactl deploy add status STATUS [flags]
```

where `STATUS` must be one of: `success`, `failed`, `canceled`, `skipped`.

Available flags for the command:

- `--endpoint`, to set the Console endpoint
- `--certificate-authority`, to provide the path to a custom CA certificate
- `--insecure-skip-tls-verify`, to disallow the check the validity of the certificate of the remote endpoint
- `--context`, to specify a different context from the currently selected one
- `--company-id`, to set the ID of the desired Company
- `--project-id`, to set the ID of the desired Project
- `--trigger-id`, to specify the trigger id to update

## extensions

The `extensions` command allows you to manage Company extensions.
Expand Down
5 changes: 5 additions & 0 deletions internal/clioptions/clioptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ type CLIOptions struct {
Revision string
DeployType string
NoSemVer bool
TriggerID string

IAMRole string
ProjectIAMRole string
Expand Down Expand Up @@ -141,6 +142,10 @@ func (o *CLIOptions) AddDeployFlags(flags *pflag.FlagSet) {
flags.BoolVar(&o.NoSemVer, "no-semver", false, "force the deploy wihout semver")
}

func (o *CLIOptions) AddDeployAddStatusFlags(flags *pflag.FlagSet) {
flags.StringVar(&o.TriggerID, "trigger-id", "", "trigger-id of the pipeline to update")
}

func (o *CLIOptions) AddContextAuthFlags(flags *pflag.FlagSet) {
flags.StringVar(&o.BasicClientID, "client-id", "", "the client ID of the service account")
flags.StringVar(&o.BasicClientSecret, "client-secret", "", "the client secret of the service account")
Expand Down
144 changes: 17 additions & 127 deletions internal/cmd/deploy/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,155 +16,45 @@
package deploy

import (
"context"
"fmt"
"time"

"github.com/mia-platform/miactl/internal/client"
"github.com/mia-platform/miactl/internal/clioptions"
"github.com/mia-platform/miactl/internal/resources"
"github.com/spf13/cobra"
)

const (
deployProjectEndpointTemplate = "/api/deploy/projects/%s/trigger/pipeline/"
pipelineStatusEndpointTemplate = "/api/deploy/projects/%s/pipelines/%d/status/"
)

func NewDeployCmd(options *clioptions.CLIOptions) *cobra.Command {
cmd := &cobra.Command{
Use: "deploy ENVIRONMENT",
Short: "Deploy the target environment.",
Long: `Trigger the deploy of the target environment in the selected project.
Long: `Deprecation Warning: This command is deprecated. Use 'deploy trigger' instead.
Trigger the deploy of the target environment in the selected project.
The deploy will be performed by the pipeline setup in project, the command will then keep
listening on updates of the status for keep the user informed on the updates. The command
will exit with error if the pipeline will not end with a success.`,
Args: cobra.ExactArgs(1),
Args: cobra.ExactArgs(1),
Deprecated: "use 'deploy trigger' instead",
RunE: func(cmd *cobra.Command, args []string) error {
environmentName := args[0]
return run(cmd.Context(), environmentName, options)
return runDeployTrigger(cmd.Context(), environmentName, options)
},
}

// set flags
flags := cmd.Flags()
options.AddConnectionFlags(flags)
options.AddContextFlags(flags)
options.AddCompanyFlags(flags)
options.AddProjectFlags(flags)
options.AddDeployFlags(flags)
if err := cmd.MarkFlagRequired("revision"); err != nil {
// if there is an error something very wrong is happening, panic
panic(err)
}

return cmd
}
deployTriggerOptions(cmd, options)

func run(ctx context.Context, environmentName string, options *clioptions.CLIOptions) error {
restConfig, err := options.ToRESTConfig()
if err != nil {
return err
}
cmd.AddCommand(
triggerCmd(options),
addCmd(options),
)

projectID := restConfig.ProjectID
if len(projectID) == 0 {
return fmt.Errorf("projectId is required to start a deploy")
}

client, err := client.APIClientForConfig(restConfig)
if err != nil {
return err
}

resp, err := triggerPipeline(ctx, client, environmentName, projectID, options)
if err != nil {
return fmt.Errorf("error executing the deploy request: %w", err)
}
fmt.Printf("Deploying project %s in the environment '%s'\n", projectID, environmentName)

status, err := waitStatus(ctx, client, projectID, resp.ID, environmentName)
if err != nil {
return fmt.Errorf("error retrieving the pipeline status: %w", err)
}

fmt.Printf("Pipeline ended with %s\n", status)
return nil
return cmd
}

func triggerPipeline(ctx context.Context, client *client.APIClient, environmentName, projectID string, options *clioptions.CLIOptions) (*resources.DeployProject, error) {
request := resources.DeployProjectRequest{
Environment: environmentName,
Revision: options.Revision,
Type: options.DeployType,
ForceDeploy: options.NoSemVer,
}

if options.DeployType == "deploy_all" {
request.ForceDeploy = true
}

requestBody, err := resources.EncodeResourceToJSON(request)
if err != nil {
return nil, fmt.Errorf("error mashalling body: %w", err)
}

resp, err := client.
Post().
APIPath(fmt.Sprintf(deployProjectEndpointTemplate, projectID)).
Body(requestBody).
Do(ctx)
if err != nil {
return nil, fmt.Errorf("error executing request: %w", err)
func addCmd(options *clioptions.CLIOptions) *cobra.Command {
cmd := &cobra.Command{
Use: "add",
}

if err := resp.Error(); err != nil {
return nil, err
}
cmd.AddCommand(newStatusAddCmd(options))

body := new(resources.DeployProject)
err = resp.ParseResponse(body)
if err != nil {
return nil, err
}

return body, nil
}

// one and a half second is the time we wait between calls to the status endpoint.
// Declared here to override it during tests
var sleepDuration = (1 * time.Second) + (500 * time.Millisecond)

func waitStatus(ctx context.Context, client *client.APIClient, projectID string, deployID int, environmentName string) (string, error) {
var outStatus *resources.PipelineStatus
for {
time.Sleep(sleepDuration)
resp, err := client.
Get().
APIPath(fmt.Sprintf(pipelineStatusEndpointTemplate, projectID, deployID)).
SetParam("environment", environmentName).
Do(ctx)

if err != nil {
return "", err
}
if err := resp.Error(); err != nil {
return "", err
}

status := new(resources.PipelineStatus)
if err := resp.ParseResponse(status); err != nil {
return "", err
}

if status.Status != "running" && status.Status != "pending" {
outStatus = status
break
}

fmt.Printf("The pipeline is %s..\n", status.Status)
}

return outStatus.Status, nil
return cmd
}
115 changes: 115 additions & 0 deletions internal/cmd/deploy/status.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// Copyright Mia srl
// SPDX-License-Identifier: Apache-2.0
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package deploy

import (
"context"
"fmt"
"strings"

"github.com/mia-platform/miactl/internal/client"
"github.com/mia-platform/miactl/internal/clioptions"
"github.com/mia-platform/miactl/internal/resources"
"github.com/mia-platform/miactl/internal/resources/deploy"
"github.com/spf13/cobra"
)

const (
deployStatusTriggerEndpointTemplate = "/api/deploy/webhooks/projects/%s/pipelines/triggers/%s/status/"
deployStatusErrorRequiredTemplate = "%s is required to update the deploy trigger status"
)

var allowedArgs = []string{"success", "failed", "canceled", "skipped"}

func newStatusAddCmd(options *clioptions.CLIOptions) *cobra.Command {
cmd := &cobra.Command{
Use: "status" + " [" + strings.Join(allowedArgs, "|") + "]",
Short: "Add status to deploy history record.",
Long: `This command is used to add a status to a deploy history record.
The status can be updated only once, using the trigger ID provided in the 'deploy trigger' command
to the pipeline.
At the moment, the only deploy trigger which creates a trigger ID is the integration with the Jenkins provider.`,
ValidArgs: allowedArgs,
Args: cobra.MatchAll(
cobra.ExactArgs(1),
cobra.OnlyValidArgs,
),
RunE: func(cmd *cobra.Command, args []string) error {
return runAddDeployStatus(cmd.Context(), options, args[0])
},
}

flags := cmd.Flags()
options.AddConnectionFlags(flags)
options.AddContextFlags(flags)
options.AddCompanyFlags(flags)
options.AddProjectFlags(flags)
options.AddDeployAddStatusFlags(flags)
if err := cmd.MarkFlagRequired("trigger-id"); err != nil {
// if there is an error something very wrong is happening, panic
panic(err)
}

return cmd
}

func runAddDeployStatus(ctx context.Context, options *clioptions.CLIOptions, status string) error {
restConfig, err := options.ToRESTConfig()
if err != nil {
return err
}

projectID := restConfig.ProjectID
if len(projectID) == 0 {
return fmt.Errorf(deployStatusErrorRequiredTemplate, "projectId")
}

client, err := client.APIClientForConfig(restConfig)
if err != nil {
return err
}

triggerID := options.TriggerID
if len(triggerID) == 0 {
return fmt.Errorf(deployStatusErrorRequiredTemplate, "triggerId")
}

requestBody := deploy.AddPipelineStatusRequest{
Status: status,
}
payload, err := resources.EncodeResourceToJSON(requestBody)
if err != nil {
return err
}

resp, err := client.Post().
APIPath(fmt.Sprintf(deployStatusTriggerEndpointTemplate, projectID, triggerID)).
Body(payload).
Do(ctx)
if err != nil {
return err
}

if err := resp.Error(); err != nil {
return err
}

fmt.Printf("Deploy status updated for pipeline with triggerId %s to %s\n", triggerID, status)

return nil
}
Loading

0 comments on commit 87efd8f

Please sign in to comment.