// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // Package plugin implements extensions for AWS CLI's lightsail subcommand. // See: https://github.com/aws/aws-cli/tree/ce7dc9a61b/awscli/customizations/lightsail package plugin import ( "bytes" "context" "crypto/tls" "encoding/json" "flag" "fmt" "io" "log" "net/http" "os" "strconv" "strings" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/aws/middleware" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/lightsail" "github.com/aws/lightsailctl/internal" "github.com/aws/lightsailctl/internal/cs" smithyMW "github.com/aws/smithy-go/middleware" ) func Main(progname string, args []string) { input, inputStdin := "", false fs := flag.NewFlagSet(progname, flag.ExitOnError) const inputFlag = "input" fs.StringVar(&input, inputFlag, "", "plugin `payload`") const inputStdinFlag = "input-stdin" fs.BoolVar(&inputStdin, inputStdinFlag, false, "receive plugin payload on stdin") _ = fs.Parse(args) if input == "" && !inputStdin { fs.Usage() log.Fatalf("no plugin input: either %q or %q flag must be specified", fs.Lookup(inputFlag).Name, fs.Lookup(inputStdinFlag).Name) } var r io.Reader = strings.NewReader(input) if inputStdin { r = os.Stdin } in, err := parseInput(r) if err != nil { log.Fatalf("invalid plugin input: %v", err) } // This is a logger used for extra diagnostics, when the debugging mode is on. debugLog := log.New(log.Writer(), log.Prefix(), log.Flags()) if !in.Configuration.Debug { debugLog.SetOutput(io.Discard) } if err := invokeOperation(context.Background(), in, debugLog); err != nil { log.Fatal(err) } } type Input struct { InputVersion string `json:"inputVersion"` Operation string `json:"operation"` Payload json.RawMessage `json:"payload"` Configuration OperationConfig `json:"configuration"` } type OperationConfig struct { Debug bool `json:"debug,omitempty"` Endpoint string `json:"endpoint,omitempty"` Region string `json:"region,omitempty"` Profile string `json:"profile,omitempty"` CABundle string `json:"caBundle,omitempty"` DoNotVerifySSL bool `json:"doNotVerifySSL,omitempty"` // CLIVersion is the version of the calling CLI, // for diagnostics and logging purposes. CLIVersion string `json:"cliVersion"` } func (c *OperationConfig) awsConfig(ctx context.Context) (aws.Config, error) { var opts []func(*config.LoadOptions) error opts = append(opts, config.WithAPIOptions([]func(*smithyMW.Stack) error{ middleware.AddUserAgentKeyValue("lightsailctl", internal.Version.String()), })) if c.Region != "" { opts = append(opts, config.WithRegion(c.Region)) } if ep := strings.TrimRight(c.Endpoint, "/"); ep != "" { opts = append(opts, config.WithEndpointResolverWithOptions( aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...interface{}) (aws.Endpoint, error) { return aws.Endpoint{URL: ep}, nil }))) } if c.Profile != "" { opts = append(opts, config.WithSharedConfigProfile(c.Profile)) } if c.Debug { opts = append(opts, config.WithClientLogMode(aws.LogSigning|aws.LogRequestWithBody|aws.LogResponseWithBody)) } if c.DoNotVerifySSL { opts = append(opts, config.WithHTTPClient(&http.Client{ Transport: &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, }, })) } if c.CABundle != "" { b, err := os.ReadFile(c.CABundle) if err != nil { return aws.Config{}, fmt.Errorf("read CA bundle file: %w", err) } opts = append(opts, config.WithCustomCABundle(bytes.NewReader(b))) } return config.LoadDefaultConfig(ctx, opts...) } func parseInput(r io.Reader) (*Input, error) { in := new(Input) if err := json.NewDecoder(r).Decode(in); err != nil { return nil, fmt.Errorf("unable to unmarshal JSON input: %v", err) } if ver, err := strconv.Atoi(in.InputVersion); err != nil || ver < 0 { return nil, fmt.Errorf("invalid inputVersion: it must contain a non-negative number") } return in, nil } func invokeOperation(ctx context.Context, in *Input, debugLog *log.Logger) error { switch in.Operation { case "PushContainerImage": cfg, err := in.Configuration.awsConfig(ctx) if err != nil { return err } ls := lightsail.NewFromConfig(cfg) internal.CheckForUpdates(ctx, debugLog, ls, internal.Version) r, err := parsePushContainerImagePayload(in.Payload) if err != nil { return fmt.Errorf("unable to parse the input's payload field: %w", err) } dc, err := cs.NewDockerEngine(ctx) if err != nil { return err } if err := cs.PushImage(ctx, r, ls, dc); err != nil { return err } default: return fmt.Errorf("unknown plugin operation: %q", in.Operation) } return nil } func parsePushContainerImagePayload(data json.RawMessage) (*cs.PushImageInput, error) { p := struct { Service string `json:"service"` Image string `json:"image"` Label string `json:"label"` }{} if err := json.Unmarshal(data, &p); err != nil { return nil, err } for _, check := range []struct{ what, input string }{ {"service name", p.Service}, {"container image", p.Image}, {"container label", p.Label}, } { if len(check.input) != 0 { continue } return nil, fmt.Errorf("push container image: %s is not specified", check.what) } return &cs.PushImageInput{Service: p.Service, Image: p.Image, Label: p.Label}, nil }