diff --git a/client/client.go b/client/client.go index 23242a9..1d18df9 100644 --- a/client/client.go +++ b/client/client.go @@ -22,22 +22,22 @@ func New(wraps *http.Client, session session.Session) Client { return &client{wraps: wraps, session: session} } -func (c *client) Mutate(ctx context.Context, mutation interface{}, variables map[string]interface{}) error { +func (c *client) Mutate(ctx context.Context, mutation interface{}, variables map[string]interface{}, opts ...graphql.RequestOption) error { apiClient, err := c.apiClient(ctx) if err != nil { return nil } - return apiClient.Mutate(ctx, mutation, variables) + return apiClient.Mutate(ctx, mutation, variables, opts...) } -func (c *client) Query(ctx context.Context, query interface{}, variables map[string]interface{}) error { +func (c *client) Query(ctx context.Context, query interface{}, variables map[string]interface{}, opts ...graphql.RequestOption) error { apiClient, err := c.apiClient(ctx) if err != nil { return nil } - return apiClient.Query(ctx, query, variables) + return apiClient.Query(ctx, query, variables, opts...) } func (c *client) URL(format string, a ...interface{}) string { diff --git a/client/interface.go b/client/interface.go index b766351..7065d1f 100644 --- a/client/interface.go +++ b/client/interface.go @@ -1,14 +1,18 @@ package client -import "context" +import ( + "context" + + "github.com/shurcooL/graphql" +) // Client abstracts away Spacelift's client API. type Client interface { // Query executes a single GraphQL query request. - Query(context.Context, interface{}, map[string]interface{}) error + Query(context.Context, interface{}, map[string]interface{}, ...graphql.RequestOption) error // Mutate executes a single GraphQL mutation request. - Mutate(context.Context, interface{}, map[string]interface{}) error + Mutate(context.Context, interface{}, map[string]interface{}, ...graphql.RequestOption) error // URL returns a full URL given a formatted path. URL(string, ...interface{}) string diff --git a/go.mod b/go.mod index 071171a..17a7ed1 100644 --- a/go.mod +++ b/go.mod @@ -13,10 +13,10 @@ require ( github.com/sabhiram/go-gitignore v0.0.0-20201211210132-54b8a0bf510f github.com/shurcooL/graphql v0.0.0-20200928012149-18c5c3165e3a github.com/urfave/cli/v2 v2.3.0 - golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 // indirect golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602 - golang.org/x/sys v0.0.0-20210412220455-f1c623a9e750 // indirect golang.org/x/term v0.0.0-20210406210042-72f3dc4e9b72 ) replace github.com/mholt/archiver/v3 => github.com/spacelift-io/archiver/v3 v3.3.1-0.20210427125142-c305b5a627ba + +replace github.com/shurcooL/graphql => github.com/marcinwyszynski/graphql v0.0.0-20210505073322-ed22d920d37d diff --git a/go.sum b/go.sum index 6dd5584..bd86726 100644 --- a/go.sum +++ b/go.sum @@ -123,6 +123,7 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m github.com/gookit/color v1.4.1/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ= github.com/gookit/color v1.4.2 h1:tXy44JFSFkKnELV6WaMo/lLfu/meqITX3iAV52do7lk= github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ= +github.com/graph-gophers/graphql-go v1.1.0/go.mod h1:9CQHMSxwO4MprSdzoIEobiHpoLtHm77vfxsvsIN5Vuc= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= @@ -141,6 +142,8 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/marcinwyszynski/graphql v0.0.0-20210505073322-ed22d920d37d h1:9Z8P/yiZQQucF5Yo3bmn0JD7Y4TrtGETsbmZpexIuxc= +github.com/marcinwyszynski/graphql v0.0.0-20210505073322-ed22d920d37d/go.mod h1:arY+KAJGvLcmVrvOSx+bDyXcfKl4+eurb19qg+FYX08= github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= @@ -157,6 +160,7 @@ github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108 github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.11.0 h1:+CqWgvj0OZycCaqclBD1pxKHAU+tOkHmQIWvDHq2aug= github.com/onsi/gomega v1.11.0/go.mod h1:azGKhqFUon9Vuj0YmTfLSmx0FUwqXYSTl5re8lQLTUg= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -172,8 +176,6 @@ github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sabhiram/go-gitignore v0.0.0-20201211210132-54b8a0bf510f h1:8P2MkG70G76gnZBOPGwmMIgwBb/rESQuwsJ7K8ds4NE= github.com/sabhiram/go-gitignore v0.0.0-20201211210132-54b8a0bf510f/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs= -github.com/shurcooL/graphql v0.0.0-20200928012149-18c5c3165e3a h1:KikTa6HtAK8cS1qjvUvvq4QO21QnwC+EfvB+OAuZ/ZU= -github.com/shurcooL/graphql v0.0.0-20200928012149-18c5c3165e3a/go.mod h1:AuYgA5Kyo4c7HfUmvRGs/6rGlMMV/6B1bVnB9JxJEEg= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/spacelift-io/archiver/v3 v3.3.1-0.20210427125142-c305b5a627ba h1:EKGvn71p8gFSqPzn/cPTbSFAOIa7z+3TmZuO6M/EWBU= @@ -262,8 +264,8 @@ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210505024714-0287a6fb4125 h1:Ugb8sMTWuWRC3+sz5WeN/4kejDx9BvIwnPUiJBjJE+8= +golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -312,8 +314,8 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210412220455-f1c623a9e750 h1:ZBu6861dZq7xBnG1bn5SRU0vA8nx42at4+kP07FMTog= -golang.org/x/sys v0.0.0-20210412220455-f1c623a9e750/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210406210042-72f3dc4e9b72 h1:VqE9gduFZ4dbR7XoL77lHFp0/DyDUBKSXK7CMFkVcV0= @@ -322,8 +324,9 @@ golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fq golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/internal/cmd/stack/constants.go b/internal/cmd/stack/constants.go new file mode 100644 index 0000000..51edcea --- /dev/null +++ b/internal/cmd/stack/constants.go @@ -0,0 +1,5 @@ +package stack + +// UserProvidedRunMetadataHeader is the HTTP header used to pass arbitrary metadata +// to runs when creating or confirming them. +const UserProvidedRunMetadataHeader = "Spacelift-User-Provided-Run-Metadata" diff --git a/internal/cmd/stack/flags.go b/internal/cmd/stack/flags.go index d7fb1b0..1f99bf5 100644 --- a/internal/cmd/stack/flags.go +++ b/internal/cmd/stack/flags.go @@ -30,6 +30,11 @@ var flagNoInit = &cli.BoolFlag{ Value: false, } +var flagRunMetadata = &cli.StringFlag{ + Name: "run-metadata", + Usage: "Additional opaque metadata you will be able to access from policies handling this Run.", +} + var flagTail = &cli.BoolFlag{ Name: "tail", Usage: "Indicate whether to tail the run", diff --git a/internal/cmd/stack/local_preview.go b/internal/cmd/stack/local_preview.go index 2cd7ee5..33e5090 100644 --- a/internal/cmd/stack/local_preview.go +++ b/internal/cmd/stack/local_preview.go @@ -11,8 +11,9 @@ import ( "github.com/mholt/archiver/v3" ignore "github.com/sabhiram/go-gitignore" "github.com/shurcooL/graphql" - "github.com/spacelift-io/spacectl/internal/cmd/authenticated" "github.com/urfave/cli/v2" + + "github.com/spacelift-io/spacectl/internal/cmd/authenticated" ) func localPreview() cli.ActionFunc { @@ -62,11 +63,16 @@ func localPreview() cli.ActionFunc { } triggerVariables := map[string]interface{}{ - "stack": graphql.ID(stackID), + "stack": graphql.ID(stackID), "workspace": graphql.ID(uploadMutation.UploadLocalWorkspace.ID), } - if err := authenticated.Client.Mutate(ctx, &triggerMutation, triggerVariables); err != nil { + var requestOpts []graphql.RequestOption + if cliCtx.IsSet(flagRunMetadata.Name) { + requestOpts = append(requestOpts, graphql.WithHeader(UserProvidedRunMetadataHeader, cliCtx.String(flagRunMetadata.Name))) + } + + if err := authenticated.Client.Mutate(ctx, &triggerMutation, triggerVariables, requestOpts...); err != nil { return err } diff --git a/internal/cmd/stack/run_confirm.go b/internal/cmd/stack/run_confirm.go new file mode 100644 index 0000000..0be2405 --- /dev/null +++ b/internal/cmd/stack/run_confirm.go @@ -0,0 +1,56 @@ +package stack + +import ( + "context" + "fmt" + + "github.com/shurcooL/graphql" + "github.com/urfave/cli/v2" + + "github.com/spacelift-io/spacectl/internal/cmd/authenticated" +) + +func runConfirm() cli.ActionFunc { + return func(cliCtx *cli.Context) error { + var mutation struct { + RunConfirm struct { + ID string `grapqhl:"id"` + } `graphql:"runConfirm(stack: $stack, run: $run)"` + } + + variables := map[string]interface{}{ + "stack": graphql.ID(stackID), + "run": graphql.ID(cliCtx.String(flagRun.Name)), + } + + ctx := context.Background() + + var requestOpts []graphql.RequestOption + if cliCtx.IsSet(flagRunMetadata.Name) { + requestOpts = append(requestOpts, graphql.WithHeader(UserProvidedRunMetadataHeader, cliCtx.String(flagRunMetadata.Name))) + } + + if err := authenticated.Client.Mutate(ctx, &mutation, variables, requestOpts...); err != nil { + return err + } + + fmt.Println("You have successfully confirmed a deployment") + + fmt.Println("The live run can be visited at", authenticated.Client.URL( + "/stack/%s/run/%s", + stackID, + mutation.RunConfirm.ID, + )) + + if !cliCtx.Bool(flagTail.Name) { + return nil + } + + terminal, err := runLogs(ctx, stackID, mutation.RunConfirm.ID) + if err != nil { + return err + } + + return terminal.Error() + } +} diff --git a/internal/cmd/stack/run_trigger.go b/internal/cmd/stack/run_trigger.go index e62402c..2b62d81 100644 --- a/internal/cmd/stack/run_trigger.go +++ b/internal/cmd/stack/run_trigger.go @@ -31,7 +31,12 @@ func runTrigger(spaceliftType, humanType string) cli.ActionFunc { ctx := context.Background() - if err := authenticated.Client.Mutate(ctx, &mutation, variables); err != nil { + var requestOpts []graphql.RequestOption + if cliCtx.IsSet(flagRunMetadata.Name) { + requestOpts = append(requestOpts, graphql.WithHeader(UserProvidedRunMetadataHeader, cliCtx.String(flagRunMetadata.Name))) + } + + if err := authenticated.Client.Mutate(ctx, &mutation, variables, requestOpts...); err != nil { return err } diff --git a/internal/cmd/stack/stack.go b/internal/cmd/stack/stack.go index 83c00fb..66ad1c2 100644 --- a/internal/cmd/stack/stack.go +++ b/internal/cmd/stack/stack.go @@ -17,12 +17,24 @@ func Command() *cli.Command { Flags: []cli.Flag{flagStackID}, Before: actions.Multi(authenticated.Ensure, beforeEach), Subcommands: []*cli.Command{ + { + Category: "Run management", + Name: "confirm", + Usage: "Confirm an unconfirmed tracked run", + Flags: []cli.Flag{ + flagRun, + flagRunMetadata, + flagTail, + }, + Action: runConfirm(), + }, { Category: "Run management", Name: "deploy", Usage: "Start a deployment (tracked run)", Flags: []cli.Flag{ flagCommitSHA, + flagRunMetadata, flagTail, }, Action: runTrigger("TRACKED", "deployment"), @@ -32,6 +44,7 @@ func Command() *cli.Command { Name: "local-preview", Usage: "Start a preview (proposed run) based on the current directory. Respects .gitignore and .terraformignore.", Flags: []cli.Flag{ + flagRunMetadata, flagNoTail, }, Action: localPreview(), @@ -52,6 +65,7 @@ func Command() *cli.Command { Usage: "Start a preview (proposed run)", Flags: []cli.Flag{ flagCommitSHA, + flagRunMetadata, flagTail, }, Action: runTrigger("PROPOSED", "preview"), @@ -69,6 +83,7 @@ func Command() *cli.Command { Usage: "Perform a task in a workspace", Flags: []cli.Flag{ flagNoInit, + flagRunMetadata, flagTail, }, Action: taskCommand, diff --git a/internal/cmd/stack/task_command.go b/internal/cmd/stack/task_command.go index b07d384..a4f4c69 100644 --- a/internal/cmd/stack/task_command.go +++ b/internal/cmd/stack/task_command.go @@ -26,7 +26,12 @@ func taskCommand(cliCtx *cli.Context) error { ctx := context.Background() - if err := authenticated.Client.Mutate(ctx, &mutation, variables); err != nil { + var requestOpts []graphql.RequestOption + if cliCtx.IsSet(flagRunMetadata.Name) { + requestOpts = append(requestOpts, graphql.WithHeader(UserProvidedRunMetadataHeader, cliCtx.String(flagRunMetadata.Name))) + } + + if err := authenticated.Client.Mutate(ctx, &mutation, variables, requestOpts...); err != nil { return err }