diff --git a/aws_provider/aws.go b/aws_provider/aws.go deleted file mode 100644 index 259f5af..0000000 --- a/aws_provider/aws.go +++ /dev/null @@ -1,580 +0,0 @@ -package aws_provider - -import ( - "bytes" - "context" - "encoding/base64" - "errors" - "fmt" - "io" - "net/http" - "os" - "path/filepath" - "strconv" - "strings" - "time" - - "github.com/aws/aws-sdk-go-v2/aws" - awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http" - "github.com/aws/aws-sdk-go-v2/config" - "github.com/aws/aws-sdk-go-v2/feature/s3/manager" - "github.com/aws/aws-sdk-go-v2/service/dynamodb" - ddbtype "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" - "github.com/aws/aws-sdk-go-v2/service/ecr" - "github.com/aws/aws-sdk-go-v2/service/iam" - "github.com/aws/aws-sdk-go-v2/service/s3" - s3type "github.com/aws/aws-sdk-go-v2/service/s3/types" - "github.com/aws/aws-sdk-go-v2/service/sts" - "github.com/aws/smithy-go" - "go.uber.org/zap" - - "rmk/system" -) - -type AwsConfigure struct { - Profile string `yaml:"profile,omitempty"` - Region string `yaml:"region,omitempty"` - AccountID string `yaml:"account_id,omitempty"` - UserName string `yaml:"user_name,omitempty"` - MFADeviceSerialNumber string `yaml:"mfa_device,omitempty"` - *MFAToken `yaml:"-"` - MFAProfileCredentials aws.Credentials `yaml:"-"` -} - -type MFAToken struct { - AccessKeyId string - Expiration time.Time - SecretAccessKey string - SessionToken string -} - -func (a *AwsConfigure) AWSSharedConfigFile(profile string) []string { - return []string{system.GetHomePath(".aws", "config_"+profile)} -} - -func (a *AwsConfigure) AWSSharedCredentialsFile(profile string) []string { - return []string{system.GetHomePath(".aws", "credentials_"+profile)} -} - -func (a *AwsConfigure) errorProxy(cfg aws.Config, err error) (aws.Config, error) { - for _, val := range cfg.ConfigSources { - switch result := val.(type) { - case config.SharedConfig: - if len(result.Profile) == 0 && len(result.Region) == 0 { - return aws.Config{}, fmt.Errorf("AWS profile by name %s does not exist, will be created", a.Profile) - } - } - } - - return cfg, err -} - -// configOptions - forming custom paths to AWS credentials and profile -func (a *AwsConfigure) configOptions() []func(options *config.LoadOptions) error { - return []func(options *config.LoadOptions) error{ - config.WithSharedConfigFiles(a.AWSSharedConfigFile(a.Profile)), - config.WithSharedCredentialsFiles(a.AWSSharedCredentialsFile(a.Profile)), - config.WithSharedConfigProfile(a.Profile), - } -} - -func (a *AwsConfigure) GetUserName() error { - ctx := context.TODO() - cfg, err := a.errorProxy(config.LoadDefaultConfig(ctx, a.configOptions()...)) - if err != nil { - return err - } - - user, err := iam.NewFromConfig(cfg).GetUser(ctx, &iam.GetUserInput{}) - if err != nil { - return err - } - - a.UserName = aws.ToString(user.User.UserName) - - return nil -} - -func (a *AwsConfigure) GetAWSCredentials() error { - ctx := context.TODO() - cfg, err := a.errorProxy(config.LoadDefaultConfig(ctx, a.configOptions()...)) - if err != nil { - return err - } - - if a.MFAProfileCredentials, err = cfg.Credentials.Retrieve(ctx); err != nil { - return err - } - - return nil -} - -func (a *AwsConfigure) GetMFADevicesSerialNumbers() error { - var serialNumbers = make(map[string]string) - - if err := a.GetUserName(); err != nil { - return err - } - - if err := a.GetAWSCredentials(); err != nil { - return err - } - - ctx := context.TODO() - cfg, err := a.errorProxy(config.LoadDefaultConfig(ctx, a.configOptions()...)) - if err != nil { - return err - } - - mfaDevices, err := iam.NewFromConfig(cfg).ListMFADevices(ctx, - &iam.ListMFADevicesInput{UserName: aws.String(a.UserName)}) - if err != nil { - return err - } - - if len(mfaDevices.MFADevices) > 1 { - for key, val := range mfaDevices.MFADevices { - fmt.Printf("%d. - MFA Device SerialNumber: %s\n", key+1, aws.ToString(val.SerialNumber)) - serialNumbers[strconv.Itoa(key+1)] = aws.ToString(val.SerialNumber) - } - - if _, ok := serialNumbers[system.ReadStdin("number SerialNumber")]; ok { - a.MFADeviceSerialNumber = serialNumbers[system.ReadStdin("number SerialNumber")] - } else { - return fmt.Errorf("incorrectly specified number SerialNumber") - } - } else if len(mfaDevices.MFADevices) == 1 { - a.MFADeviceSerialNumber = aws.ToString(mfaDevices.MFADevices[0].SerialNumber) - } - - return nil -} - -func (a *AwsConfigure) GetMFASessionToken() error { - ctx := context.TODO() - cfg, err := a.errorProxy(config.LoadDefaultConfig(ctx, a.configOptions()...)) - if err != nil { - return err - } - - if err := a.GetMFADevicesSerialNumbers(); err != nil { - return err - } - - token, err := sts.NewFromConfig(cfg).GetSessionToken(ctx, &sts.GetSessionTokenInput{ - DurationSeconds: aws.Int32(43200), - SerialNumber: aws.String(a.MFADeviceSerialNumber), - TokenCode: aws.String(system.ReadStdin("TOTP")), - }) - if err != nil { - return err - } - - a.MFAToken = &MFAToken{ - AccessKeyId: aws.ToString(token.Credentials.AccessKeyId), - Expiration: aws.ToTime(token.Credentials.Expiration), - SecretAccessKey: aws.ToString(token.Credentials.SecretAccessKey), - SessionToken: aws.ToString(token.Credentials.SessionToken), - } - - return nil -} - -func (a *AwsConfigure) GetAwsConfigure(profile string) (bool, error) { - ctx := context.TODO() - cfg, err := a.errorProxy(config.LoadDefaultConfig(ctx, - config.WithSharedConfigFiles(a.AWSSharedConfigFile(profile)), - config.WithSharedCredentialsFiles(a.AWSSharedCredentialsFile(profile)), - config.WithSharedConfigProfile(profile), - )) - if err != nil { - return true, err - } - - client := sts.NewFromConfig(cfg) - identity, err := client.GetCallerIdentity(ctx, &sts.GetCallerIdentityInput{}) - if err != nil { - return false, err - } - - a.Region = cfg.Region - a.AccountID = aws.ToString(identity.Account) - - return true, nil -} - -func (a *AwsConfigure) GetECRCredentials(region string) (map[string]string, error) { - ctx := context.TODO() - ecrCredentials := make(map[string]string) - - cfg, err := a.errorProxy(config.LoadDefaultConfig(ctx, a.configOptions()...)) - if err != nil { - return nil, err - } - - // needed for specific AWS account where ECR used - cfg.Region = region - - svc := ecr.NewFromConfig(cfg) - token, err := svc.GetAuthorizationToken(ctx, &ecr.GetAuthorizationTokenInput{}) - if err != nil { - return nil, err - } - - authData := token.AuthorizationData[0].AuthorizationToken - data, err := base64.StdEncoding.DecodeString(*authData) - if err != nil { - return nil, err - } - - parts := strings.SplitN(string(data), ":", 2) - if len(parts) < 2 { - return nil, fmt.Errorf("it is impossible to get ECR user and password "+ - "for current AWS profile: %s", a.Profile) - } - - ecrCredentials[parts[0]] = parts[1] - - return ecrCredentials, nil -} - -func (a *AwsConfigure) CreateBucket(bucketName string) error { - var ( - respError s3.ResponseError - bucketExist *s3type.BucketAlreadyExists - bucketOwner *s3type.BucketAlreadyOwnedByYou - bucketParams s3.CreateBucketInput - bucketNotFound *s3type.NotFound - ) - - ctx := context.TODO() - cfg, err := a.errorProxy(config.LoadDefaultConfig(ctx, a.configOptions()...)) - if err != nil { - return err - } - - client := s3.NewFromConfig(cfg) - - if a.Region == system.RegionException { - _, err := client.HeadBucket(ctx, &s3.HeadBucketInput{Bucket: aws.String(bucketName)}) - if err != nil { - if !errors.As(err, &bucketNotFound) { - return err - } - } else { - zap.S().Infof("S3 bucket %s already exists", bucketName) - return nil - } - - bucketParams = s3.CreateBucketInput{Bucket: aws.String(bucketName)} - } else { - bucketParams = s3.CreateBucketInput{ - Bucket: aws.String(bucketName), - CreateBucketConfiguration: &s3type.CreateBucketConfiguration{ - LocationConstraint: s3type.BucketLocationConstraint(a.Region), - }, - } - } - - resp, err := client.CreateBucket(ctx, &bucketParams) - if err != nil { - var responseError *awshttp.ResponseError - if errors.As(err, &bucketExist) { - zap.S().Infof("S3 bucket %s already exists", bucketName) - return nil - } else if errors.As(err, &bucketOwner) { - zap.S().Infof("S3 bucket %s already exists and owned by you", bucketName) - return nil - } else if errors.As(err, &responseError) && responseError.ResponseError.HTTPStatusCode() == http.StatusForbidden { - zap.S().Warnf("S3 bucket %s is not created, you don't have permissions", bucketName) - return nil - } else if errors.As(err, &respError) { - return fmt.Errorf("requestID %s, hostID %s request failure by error: %v", respError.ServiceRequestID(), - respError.ServiceHostID(), respError.Error()) - } - - return err - } - - putParams := s3.PutPublicAccessBlockInput{ - Bucket: aws.String(bucketName), - PublicAccessBlockConfiguration: &s3type.PublicAccessBlockConfiguration{ - BlockPublicAcls: aws.Bool(true), - BlockPublicPolicy: aws.Bool(true), - IgnorePublicAcls: aws.Bool(true), - RestrictPublicBuckets: aws.Bool(true), - }, - } - - if _, err := client.PutPublicAccessBlock(ctx, &putParams); err != nil { - if errors.As(err, &respError) { - return fmt.Errorf("requestID %s, hostID %s request failure by error: %v", - respError.ServiceRequestID(), respError.ServiceHostID(), respError.Error()) - } - - return err - } - - zap.S().Infof("created S3 bucket: %s - %s", bucketName, *resp.Location) - - return nil -} - -func (a *AwsConfigure) CreateDynamoDBTable(tableName string) error { - ctx := context.TODO() - cfg, err := a.errorProxy(config.LoadDefaultConfig(ctx, a.configOptions()...)) - if err != nil { - return err - } - - client := dynamodb.NewFromConfig(cfg) - dTableParams := &dynamodb.CreateTableInput{ - AttributeDefinitions: []ddbtype.AttributeDefinition{ - { - AttributeName: aws.String("LockID"), - AttributeType: ddbtype.ScalarAttributeTypeS, - }, - }, - KeySchema: []ddbtype.KeySchemaElement{ - { - AttributeName: aws.String("LockID"), - KeyType: ddbtype.KeyTypeHash, - }, - }, - TableName: aws.String(tableName), - BillingMode: ddbtype.BillingModePayPerRequest, - } - - resp, err := client.CreateTable(ctx, dTableParams) - if err != nil { - var ( - tableExist *ddbtype.ResourceInUseException - accessDenied smithy.APIError - operation *smithy.OperationError - ) - - if errors.As(err, &tableExist) { - zap.S().Infof("DynamoDB table %s already exists", tableName) - return nil - } else if errors.As(err, &accessDenied) && errors.As(err, &operation) && - operation.Operation() == "CreateTable" && accessDenied.ErrorCode() == "AccessDeniedException" { - zap.S().Warnf("DynamoDB table %s is not created, you don't have permissions", tableName) - return nil - } - - return err - } - - zap.S().Infof("created DynamoDB table: %s - %s", tableName, *resp.TableDescription.TableArn) - - return nil -} - -func (a *AwsConfigure) BucketKeyExists(region, bucketName, key string) (bool, error) { - if len(bucketName) == 0 { - return false, nil - } - - ctx := context.TODO() - cfg, err := a.errorProxy(config.LoadDefaultConfig(ctx, a.configOptions()...)) - if err != nil { - return false, err - } - - // needed for specific AWS account where S3 used - if len(region) > 0 { - cfg.Region = region - } - - client := s3.NewFromConfig(cfg) - _, err = client.HeadObject(ctx, &s3.HeadObjectInput{ - Bucket: aws.String(bucketName), - Key: aws.String(key), - }) - if err != nil { - var responseError *awshttp.ResponseError - if errors.As(err, &responseError) && responseError.ResponseError.HTTPStatusCode() == http.StatusNotFound { - return false, nil - } - - return false, err - } - - return true, nil -} - -// S3ListObjectsAPI defines the interface for the ListObjectsV2 function. -// We use this interface to test the function using a mocked service. -type S3ListObjectsAPI interface { - ListObjectsV2(ctx context.Context, - params *s3.ListObjectsV2Input, - optFns ...func(*s3.Options)) (*s3.ListObjectsV2Output, error) -} - -// GetObjects retrieves the objects in an Amazon Simple Storage Service (Amazon S3) bucket -// Inputs: -// -// c is the context of the method call, which includes the AWS Region -// api is the interface that defines the method call -// input defines the input arguments to the service call. -// -// Output: -// -// If success, a ListObjectsV2Output object containing the result of the service call and nil -// Otherwise, nil and an error from the call to ListObjectsV2 -func GetObjects(c context.Context, api S3ListObjectsAPI, input *s3.ListObjectsV2Input) (*s3.ListObjectsV2Output, error) { - return api.ListObjectsV2(c, input) -} - -func (a *AwsConfigure) DownloadFromBucket(region, bucketName, localDir, filePrefix string) error { - var noSuchBucket *s3type.NoSuchBucket - - downloadToFile := func(downloader *manager.Downloader, targetDirectory, bucket, key string) error { - // Create the directories in the path - file := filepath.Join(targetDirectory, key) - if err := os.MkdirAll(filepath.Dir(file), 0775); err != nil { - return err - } - - fd, err := os.Create(file) - if err != nil { - return err - } - defer func(fd *os.File) { - err := fd.Close() - if err != nil { - - } - }(fd) - - // Download the file using the AWS SDK for Go - zap.S().Infof("downloading s3://%s/%s to %s", bucket, key, file) - _, err = downloader.Download(context.TODO(), fd, &s3.GetObjectInput{Bucket: &bucket, Key: &key}) - - return err - } - - ctx := context.TODO() - cfg, err := a.errorProxy(config.LoadDefaultConfig(ctx, a.configOptions()...)) - if err != nil { - return err - } - - // needed for specific AWS account where S3 used - if len(region) > 0 { - cfg.Region = region - } - - client := s3.NewFromConfig(cfg) - m := manager.NewDownloader(client) - paginator := s3.NewListObjectsV2Paginator(client, &s3.ListObjectsV2Input{Bucket: aws.String(bucketName)}) - - for paginator.HasMorePages() { - page, err := paginator.NextPage(context.TODO()) - if err != nil { - if errors.As(err, &noSuchBucket) { - return fmt.Errorf("specified bucket %s does not exist", bucketName) - } - - return err - } - - for _, obj := range page.Contents { - if strings.HasPrefix(aws.ToString(obj.Key), filePrefix) { - if err := downloadToFile(m, localDir, bucketName, aws.ToString(obj.Key)); err != nil { - return err - } - } - } - } - - input := &s3.ListObjectsV2Input{ - Bucket: aws.String(bucketName), - } - - resp, err := GetObjects(context.TODO(), client, input) - if err != nil { - return err - } - - if len(resp.Contents) == 0 { - zap.S().Warnf("S3 bucket %s is empty, files do not exist", bucketName) - return nil - } - - return nil -} - -func (a *AwsConfigure) GetFileData(bucketName, key string) ([]byte, error) { - var client *s3.Client - ctx := context.TODO() - if len(a.Profile) > 0 { - cfg, err := a.errorProxy(config.LoadDefaultConfig(ctx, a.configOptions()...)) - if err != nil { - return nil, err - } - - client = s3.NewFromConfig(cfg) - } else { - client = s3.NewFromConfig(aws.Config{Region: a.Region}) - } - - input := &s3.GetObjectInput{ - Bucket: aws.String(bucketName), - Key: aws.String(key), - } - - resp, err := client.GetObject(ctx, input) - if err != nil { - return nil, err - } - - return io.ReadAll(resp.Body) -} - -func (a *AwsConfigure) UploadToBucket(bucketName, localDir, pattern string) error { - ctx := context.TODO() - cfg, err := a.errorProxy(config.LoadDefaultConfig(ctx, a.configOptions()...)) - if err != nil { - return err - } - - uploader := manager.NewUploader(s3.NewFromConfig(cfg)) - - match, err := system.WalkMatch(localDir, pattern) - if err != nil { - return err - } - - for _, path := range match { - if filepath.Base(path) != system.SopsAgeKeyFile { - data, err := os.ReadFile(path) - if err != nil { - return err - } - - result, err := uploader.Upload(context.TODO(), &s3.PutObjectInput{ - Bucket: aws.String(bucketName), - Key: aws.String(filepath.Base(path)), - Body: bytes.NewReader(data), - }) - if err != nil { - var responseError *awshttp.ResponseError - if errors.As(err, &responseError) && responseError.ResponseError.HTTPStatusCode() == http.StatusNotFound { - return fmt.Errorf("specified bucket %s does not exist", bucketName) - } - - if errors.As(err, &responseError) && responseError.ResponseError.HTTPStatusCode() == http.StatusForbidden { - return fmt.Errorf("you are not permitted to upload SOPS age keys for specified bucket %s, "+ - "access denied", bucketName) - } - - return err - } - - zap.S().Infof("uploading %s... to %s", path, result.Location) - } - } - - return nil -} diff --git a/cmd/cluster.go b/cmd/cluster.go new file mode 100644 index 0000000..10b1346 --- /dev/null +++ b/cmd/cluster.go @@ -0,0 +1,528 @@ +package cmd + +import ( + "fmt" + "go.uber.org/zap" + "os" + "regexp" + "strings" + + yaml2 "github.com/ghodss/yaml" + "github.com/urfave/cli/v2" + "gopkg.in/yaml.v3" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/json" + "k8s.io/client-go/tools/clientcmd" + clientcmdlatest "k8s.io/client-go/tools/clientcmd/api/latest" + + "rmk/config" + "rmk/providers/aws_provider" + "rmk/providers/azure_provider" + "rmk/providers/google_provider" + "rmk/util" +) + +type clusterRunner interface { + getKubeContext() (string, string, error) + switchKubeContext() error +} + +type ClusterCommands struct { + *ReleaseCommands +} + +type ClusterCTLConfig struct { + ApiVersion string + Kind string + Metadata map[string]string + Spec interface{} +} + +func newClusterCommands(conf *config.Config, ctx *cli.Context, workDir string) *ClusterCommands { + return &ClusterCommands{&ReleaseCommands{Conf: conf, Ctx: ctx, WorkDir: workDir}} +} + +func (cc *ClusterCommands) clusterCTL(args ...string) *util.SpecCMD { + var envs []string + + switch cc.Conf.ClusterProvider { + case aws_provider.AWSClusterProvider: + envs = []string{ + "AWS_B64ENCODED_CREDENTIALS=", + "CAPA_EKS_IAM=true", + "CAPA_EKS_ADD_ROLES=true", + "EXP_MACHINE_POOL=true", + } + case azure_provider.AzureClusterProvider: + envs = []string{ + "EXP_AKS=true", + "EXP_MACHINE_POOL=true", + "EXP_CLUSTER_RESOURCE_SET=false", + } + case google_provider.GoogleClusterProvider: + envs = []string{ + "GCP_B64ENCODED_CREDENTIALS=", + "EXP_CAPG_GKE=true", + "EXP_MACHINE_POOL=true", + } + } + + return &util.SpecCMD{ + Args: args, + Command: "clusterctl", + Ctx: cc.Ctx.Context, + Dir: cc.WorkDir, + Debug: true, + Envs: envs, + } +} + +func (cc *ClusterCommands) kubectl(args ...string) *util.SpecCMD { + return &util.SpecCMD{ + Args: args, + Command: "kubectl", + Ctx: cc.Ctx.Context, + Dir: cc.WorkDir, + DisableStdOut: false, + Debug: true, + } +} + +func createManifestFile(object interface{}, dir, fileName string) (string, error) { + data, err := json.Marshal(object) + if err != nil { + return "", err + } + + return util.CreateTempYAMLFile(dir, fileName, data) +} + +func createClusterCTLConfigFile(output []byte) (string, error) { + clusterCTL := &ClusterCTLConfig{} + if err := yaml.Unmarshal(output, &clusterCTL); err != nil { + return "", err + } + + data, err := yaml.Marshal(clusterCTL.Spec) + if err != nil { + return "", err + } + + return util.CreateTempYAMLFile(os.TempDir(), clusterCTL.Metadata["name"], data) +} + +func (cc *ClusterCommands) getClusterCTLConfig() (string, string, error) { + if cc.Ctx.Command.Category == util.CAPI { + cc.APICluster = true + } + + _, currentContextName, err := cc.getKubeContext() + if err != nil { + return "", "", err + } + + cc.SpecCMD = cc.prepareHelmfile("--log-level", "error", "-l", "config=clusterctl", "template") + cc.SpecCMD.DisableStdOut = true + if err := releaseRunner(cc).runCMD(); err != nil { + return "", "", fmt.Errorf("Helmfile failed to render template by release label: config=clusterctl\n%s", + cc.SpecCMD.StderrBuf.String()) + } + + fileName, err := createClusterCTLConfigFile(cc.SpecCMD.StdoutBuf.Bytes()) + if err != nil { + return "", "", err + } + + return currentContextName, fileName, nil +} + +func (cc *ClusterCommands) initClusterCTLConfig() error { + contextName, clusterCTLConfig, err := cc.getClusterCTLConfig() + if err != nil { + return err + } + + cc.SpecCMD = cc.clusterCTL("init", "--infrastructure", cc.Conf.ClusterProvider, + "--wait-providers", "--kubeconfig-context", contextName, "--config", clusterCTLConfig) + + if err := releaseRunner(cc).runCMD(); err != nil { + if err := os.RemoveAll(clusterCTLConfig); err != nil { + return err + } + + return err + } + + return os.RemoveAll(clusterCTLConfig) +} + +func (cc *ClusterCommands) manageKubeConfigItem(itemType, itemName string) error { + cc.SpecCMD = cc.kubectl("config", itemType, itemName) + cc.SpecCMD.DisableStdOut = true + if err := releaseRunner(cc).runCMD(); err != nil { + return fmt.Errorf("%s", strings.ReplaceAll(cc.SpecCMD.StderrBuf.String(), "\n", "")) + } + + zap.S().Infof("%s", strings.ReplaceAll(cc.SpecCMD.StdoutBuf.String(), "\n", "")) + + return nil +} + +func (cc *ClusterCommands) mergeKubeConfigs(clusterContext []byte) error { + var object interface{} + + if err := yaml.Unmarshal(clusterContext, &object); err != nil { + return err + } + + file, err := createManifestFile(object, os.TempDir(), cc.Conf.Name+"-"+cc.Conf.ClusterProvider+"-kubeconfig") + if err != nil { + return err + } + + loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() + loadingRules.Precedence = append(loadingRules.Precedence, file) + + mergeConfig, err := loadingRules.Load() + if err != nil { + if err := os.RemoveAll(file); err != nil { + return err + } + + return err + } + + if err := os.RemoveAll(file); err != nil { + return err + } + + data, err := runtime.Encode(clientcmdlatest.Codec, mergeConfig) + if err != nil { + return err + } + + kubeConfig, err := yaml2.JSONToYAML(data) + if err != nil { + return err + } + + if err := os.WriteFile(clientcmd.NewDefaultPathOptions().GlobalFile, kubeConfig, 0644); err != nil { + return err + } + + return cc.manageKubeConfigItem("use-context", cc.Conf.Name) +} + +func (cc *ClusterCommands) getKubeContext() (string, string, error) { + var ( + contextNames []string + contextName string + ) + + kubeConfig, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig( + clientcmd.NewDefaultClientConfigLoadingRules(), &clientcmd.ConfigOverrides{}).RawConfig() + if err != nil { + return "", "", err + } + + re, err := regexp.Compile(`(?i)\b` + cc.Conf.Name + `\b`) + if err != nil { + return "", "", err + } + + for key := range kubeConfig.Contexts { + if re.MatchString(key) { + contextNames = append(contextNames, key) + } + } + + switch { + case len(contextNames) > 1: + return "", "", + fmt.Errorf("detected more than one Kubernetes context with names %s leading to conflict, "+ + "please delete or rename all contexts except one", strings.Join(contextNames, ", ")) + case len(contextNames) > 0: + contextName = contextNames[0] + default: + contextName = "" + } + + if cc.K3DCluster && len(contextName) > 0 && !strings.Contains(contextName, util.K3DPrefix) { + return "", "", fmt.Errorf("remote Kubernetes context already exists %s for this branch", contextName) + } + + return contextName, kubeConfig.CurrentContext, nil +} + +func (cc *ClusterCommands) switchKubeContext() error { + contextName, currentContextName, err := cc.getKubeContext() + if err != nil { + return err + } + + if cc.Ctx.Command.Category == util.CAPI { + kubeConfig, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig( + clientcmd.NewDefaultClientConfigLoadingRules(), &clientcmd.ConfigOverrides{}).RawConfig() + if err != nil { + return err + } + + if _, ok := kubeConfig.Contexts[util.CAPIContextName]; ok && currentContextName != util.CAPIContextName { + return cc.manageKubeConfigItem("use-context", util.CAPIContextName) + } else if ok && currentContextName == util.CAPIContextName { + return nil + } + } + + if len(contextName) > 0 && !cc.UpdateContext { + if contextName != currentContextName { + return cc.manageKubeConfigItem("use-context", contextName) + } + + return nil + } + + if strings.Contains(contextName, util.K3DPrefix) && cc.UpdateContext { + return fmt.Errorf("current context %s already used for K3D cluster, --force flag cannot be used", contextName) + } + + switch cc.Conf.ClusterProvider { + case aws_provider.AWSClusterProvider: + clusterContext, err := cc.getAWSClusterContext() + if err != nil { + return err + } + + if err := cc.mergeKubeConfigs(clusterContext); err != nil { + return err + } + case azure_provider.AzureClusterProvider: + clusterContext, err := cc.getAzureClusterContext() + if err != nil { + return err + } + + if err := cc.mergeKubeConfigs(clusterContext); err != nil { + return err + } + case google_provider.GoogleClusterProvider: + clusterContext, err := cc.getGCPClusterContext() + if err != nil { + return err + } + + if err := cc.mergeKubeConfigs(clusterContext); err != nil { + return err + } + } + + return nil +} + +func (cc *ClusterCommands) provisionDestroyTargetCluster() error { + if cc.Ctx.Command.Category == util.CAPI { + cc.APICluster = true + } + + _, _, err := cc.getKubeContext() + if err != nil { + return err + } + + switch cc.Ctx.Command.Name { + case "provision": + switch cc.Conf.ClusterProvider { + case aws_provider.AWSClusterProvider: + if err := cc.createAWSClusterSSHKey(); err != nil { + return err + } + + if err := cc.createAWSSecrets(); err != nil { + return err + } + case azure_provider.AzureClusterProvider: + if err := cc.createAzureSecrets(cc.Conf.AzureConfigure); err != nil { + return err + } + case google_provider.GoogleClusterProvider: + if err := cc.createGCPNATGateway(); err != nil { + return err + } + + if err := cc.createGCPSecrets(); err != nil { + return err + } + } + + cc.SpecCMD = cc.prepareHelmfile("--log-level", "error", "-l", "cluster="+cc.Conf.ClusterProvider, "sync") + if err := releaseRunner(cc).runCMD(); err != nil { + return err + } + + switch cc.Conf.ClusterProvider { + case aws_provider.AWSClusterProvider: + clusterContext, err := cc.getAWSClusterContext() + if err != nil { + return err + } + + if err := cc.mergeKubeConfigs(clusterContext); err != nil { + return err + } + case azure_provider.AzureClusterProvider: + clusterContext, err := cc.getAzureClusterContext() + if err != nil { + return err + } + + if err := cc.mergeKubeConfigs(clusterContext); err != nil { + return err + } + case google_provider.GoogleClusterProvider: + clusterContext, err := cc.getGCPClusterContext() + if err != nil { + return err + } + + if err := cc.mergeKubeConfigs(clusterContext); err != nil { + return err + } + } + case "destroy": + cc.SpecCMD = cc.prepareHelmfile("--log-level", "error", "-l", "cluster="+cc.Conf.ClusterProvider, "destroy") + if err := releaseRunner(cc).runCMD(); err != nil { + return err + } + + switch cc.Conf.ClusterProvider { + case aws_provider.AWSClusterProvider: + if err := cc.deleteAWSClusterSSHKey(); err != nil { + return err + } + case azure_provider.AzureClusterProvider: + // Pre-destroy hook for Azure + case google_provider.GoogleClusterProvider: + if err := cc.deleteGCPNATGateway(); err != nil { + return err + } + } + + kubeConfig, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig( + clientcmd.NewDefaultClientConfigLoadingRules(), &clientcmd.ConfigOverrides{}).RawConfig() + if err != nil { + return err + } + + if context, ok := kubeConfig.Contexts[cc.Conf.Name]; ok { + if err := cc.manageKubeConfigItem("delete-context", cc.Conf.Name); err != nil { + return err + } + + if err := cc.manageKubeConfigItem("delete-cluster", context.Cluster); err != nil { + return err + } + + if err := cc.manageKubeConfigItem("delete-user", context.AuthInfo); err != nil { + return err + } + } + } + + return nil +} + +func clusterSwitchAction(conf *config.Config) cli.ActionFunc { + return func(c *cli.Context) error { + if err := util.ValidateNArg(c, 0); err != nil { + return err + } + + if err := resolveDependencies(conf.InitConfig(), c, false); err != nil { + return err + } + + cc := newClusterCommands(conf, c, util.GetPwdPath("")) + cc.UpdateContext = c.Bool("force") + + return cc.switchKubeContext() + } +} + +func CAPIInitAction(conf *config.Config) cli.AfterFunc { + return func(c *cli.Context) error { + if err := util.ValidateNArg(c, 0); err != nil { + return err + } + + cc := newClusterCommands(conf, c, util.GetPwdPath()) + if err := cc.switchKubeContext(); err != nil { + return err + } + + if err := cc.initClusterCTLConfig(); err != nil { + return err + } + + switch cc.Conf.ClusterProvider { + case aws_provider.AWSClusterProvider: + return cc.applyAWSClusterIdentity() + case azure_provider.AzureClusterProvider: + return cc.applyAzureClusterIdentity() + case google_provider.GoogleClusterProvider: + return cc.applyGCPClusterIdentitySecret() + } + + return nil + } +} + +func CAPIUpdateAction(conf *config.Config) cli.ActionFunc { + return func(c *cli.Context) error { + if err := util.ValidateNArg(c, 0); err != nil { + return err + } + + if err := resolveDependencies(conf.InitConfig(), c, false); err != nil { + return err + } + + cc := newClusterCommands(conf, c, util.GetPwdPath()) + if err := cc.switchKubeContext(); err != nil { + return err + } + + if err := cc.initClusterCTLConfig(); err != nil { + return err + } + + switch cc.Conf.ClusterProvider { + case aws_provider.AWSClusterProvider: + return cc.applyAWSClusterIdentity() + case azure_provider.AzureClusterProvider: + return cc.applyAzureClusterIdentity() + case google_provider.GoogleClusterProvider: + return cc.applyGCPClusterIdentitySecret() + } + + return nil + } +} + +func CAPIProvisionDestroyAction(conf *config.Config) cli.ActionFunc { + return func(c *cli.Context) error { + if err := util.ValidateNArg(c, 0); err != nil { + return err + } + + if err := resolveDependencies(conf.InitConfig(), c, false); err != nil { + return err + } + + cc := newClusterCommands(conf, c, util.GetPwdPath()) + if err := cc.switchKubeContext(); err != nil { + return err + } + + return cc.provisionDestroyTargetCluster() + } +} diff --git a/cmd/cluster_capa.go b/cmd/cluster_capa.go new file mode 100644 index 0000000..dba5395 --- /dev/null +++ b/cmd/cluster_capa.go @@ -0,0 +1,210 @@ +package cmd + +import ( + "os" + "path/filepath" + "strings" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + v1 "k8s.io/client-go/applyconfigurations/core/v1" + + "rmk/providers/aws_provider" + "rmk/util" +) + +const ( + awsFlagsCategory = "AWS authentication" + + awsIAMControllerCredentialsTemplate = `[default] +aws_access_key_id = {{ .AwsCredentialsProfile.AccessKeyID }} +aws_secret_access_key = {{ .AwsCredentialsProfile.SecretAccessKey }} +region = {{ .Region }} +{{- if .AwsCredentialsProfile.SessionToken }} +aws_session_token = {{ .AwsCredentialsProfile.SessionToken }} +{{- end }} +` + awsIAMControllerSecret = "aws-iam-controller-secret" + awsClusterStaticIdentityName = "aws-cluster-identity" + awsClusterStaticIdentityNamespace = "capa-system" + awsClusterStaticIdentitySecret = "aws-cluster-identity-secret" +) + +var awsClusterStaticIdentitySecretType = corev1.SecretTypeOpaque + +type AWSClusterStaticIdentityConfig struct { + *AWSClusterStaticIdentity + *v1.SecretApplyConfiguration + AWSIAMControllerSecret *v1.SecretApplyConfiguration + ManifestFiles []string + ManifestFilesDir string +} + +type AWSClusterStaticIdentity struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec AWSClusterStaticIdentitySpec `json:"spec,omitempty"` +} + +type AWSClusterStaticIdentitySpec struct { + AllowedNamespaces struct { + NamespaceList []string `json:"list"` + Selector *metav1.LabelSelector `json:"selector,omitempty"` + } `json:"allowedNamespaces,omitempty"` + SecretRef string `json:"secretRef"` +} + +func NewAWSClusterStaticIdentityConfig(ac *aws_provider.AwsConfigure) *AWSClusterStaticIdentityConfig { + acic := &AWSClusterStaticIdentityConfig{ + AWSClusterStaticIdentity: &AWSClusterStaticIdentity{ + TypeMeta: metav1.TypeMeta{ + Kind: "AWSClusterStaticIdentity", + APIVersion: "infrastructure.cluster.x-k8s.io/v1beta2", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: awsClusterStaticIdentityName, + Namespace: awsClusterStaticIdentityNamespace, + Labels: map[string]string{"clusterctl.cluster.x-k8s.io/move-hierarchy": "true"}, + }, + Spec: AWSClusterStaticIdentitySpec{ + AllowedNamespaces: struct { + NamespaceList []string `json:"list"` + Selector *metav1.LabelSelector `json:"selector,omitempty"` + }(struct { + NamespaceList []string + Selector *metav1.LabelSelector + }{ + NamespaceList: []string{awsClusterStaticIdentityNamespace}, + }), + SecretRef: awsClusterStaticIdentitySecret, + }, + }, + AWSIAMControllerSecret: v1.Secret(awsIAMControllerSecret, awsClusterStaticIdentityNamespace), + SecretApplyConfiguration: v1.Secret(awsClusterStaticIdentitySecret, awsClusterStaticIdentityNamespace), + ManifestFilesDir: filepath.Join(os.TempDir(), awsClusterStaticIdentityName), + } + + profile, err := ac.RenderAWSConfigProfile(awsIAMControllerCredentialsTemplate) + if err != nil { + return nil + } + + acic.AWSIAMControllerSecret.Type = &awsClusterStaticIdentitySecretType + acic.AWSIAMControllerSecret.Data = map[string][]byte{"credentials": profile} + + acic.SecretApplyConfiguration.Type = &awsClusterStaticIdentitySecretType + acic.SecretApplyConfiguration.Data = map[string][]byte{ + "AccessKeyID": []byte(ac.AwsCredentialsProfile.AccessKeyID), + "SecretAccessKey": []byte(ac.AwsCredentialsProfile.SecretAccessKey), + } + + if len(ac.AwsCredentialsProfile.SessionToken) > 0 { + acic.SecretApplyConfiguration.Data["SessionToken"] = []byte(ac.AwsCredentialsProfile.SessionToken) + } + + return acic +} + +func (acic *AWSClusterStaticIdentityConfig) createAWSClusterIdentityManifestFiles() error { + if err := os.MkdirAll(acic.ManifestFilesDir, 0775); err != nil { + return err + } + + fileCR, err := createManifestFile(acic.AWSClusterStaticIdentity, acic.ManifestFilesDir, awsClusterStaticIdentityName) + if err != nil { + return err + } + + acic.ManifestFiles = append(acic.ManifestFiles, fileCR) + + fileCRSecret, err := createManifestFile(acic.SecretApplyConfiguration, acic.ManifestFilesDir, awsClusterStaticIdentitySecret) + if err != nil { + return err + } + + acic.ManifestFiles = append(acic.ManifestFiles, fileCRSecret) + + fileIAMControllerSecret, err := createManifestFile(acic.AWSIAMControllerSecret, acic.ManifestFilesDir, awsIAMControllerSecret) + if err != nil { + return err + } + + acic.ManifestFiles = append(acic.ManifestFiles, fileIAMControllerSecret) + + return nil +} + +func (cc *ClusterCommands) applyAWSClusterIdentity() error { + var kubectlArgs = []string{"apply"} + + ac := aws_provider.NewAwsConfigure(cc.Ctx.Context, cc.Conf.Profile) + if err := ac.ReadAWSConfigProfile(); err != nil { + return err + } + + acic := NewAWSClusterStaticIdentityConfig(ac) + if err := acic.createAWSClusterIdentityManifestFiles(); err != nil { + return err + } + + for _, val := range acic.ManifestFiles { + kubectlArgs = append(kubectlArgs, "-f", val) + } + + cc.SpecCMD = cc.kubectl(kubectlArgs...) + if err := releaseRunner(cc).runCMD(); err != nil { + if err := os.RemoveAll(acic.ManifestFilesDir); err != nil { + return err + } + + return err + } + + return os.RemoveAll(acic.ManifestFilesDir) +} + +func (cc *ClusterCommands) getAWSClusterContext() ([]byte, error) { + return aws_provider.NewAwsConfigure(cc.Ctx.Context, cc.Conf.Profile).GetAWSClusterContext(cc.Conf.Name) +} + +func (cc *ClusterCommands) createAWSClusterSSHKey() error { + return aws_provider.NewAwsConfigure(cc.Ctx.Context, cc.Conf.Profile).CreateAWSEC2SSHKey(cc.Conf.Name) +} + +func (cc *ClusterCommands) deleteAWSClusterSSHKey() error { + return aws_provider.NewAwsConfigure(cc.Ctx.Context, cc.Conf.Profile).DeleteAWSEC2SSHKey(cc.Conf.Name) +} + +func (cc *ClusterCommands) createAWSSecrets() error { + a := aws_provider.NewAwsConfigure(cc.Ctx.Context, cc.Conf.Profile) + + secrets, err := a.GetAWSSecrets(cc.Conf.Tenant) + if err != nil { + return err + } + + if len(secrets) > 0 { + return nil + } + + walkMatch, err := util.WalkMatch(cc.Conf.SopsAgeKeys, cc.Conf.Tenant+"*"+util.SopsAgeKeyExt) + if err != nil { + return err + } + + for _, val := range walkMatch { + file, err := os.ReadFile(val) + if err != nil { + return err + } + + keyName := strings.TrimSuffix(filepath.Base(val), util.SopsAgeKeyExt) + + if err := a.SetAWSSecret(cc.Conf.Tenant, keyName, file); err != nil { + return err + } + } + + return nil +} diff --git a/cmd/cluster_capg.go b/cmd/cluster_capg.go new file mode 100644 index 0000000..6064e2d --- /dev/null +++ b/cmd/cluster_capg.go @@ -0,0 +1,132 @@ +package cmd + +import ( + "os" + "path/filepath" + "strings" + + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/client-go/applyconfigurations/core/v1" + + "rmk/providers/google_provider" + "rmk/util" +) + +const ( + gcpClusterIdentityName = "gcp-cluster-identity" + gcpClusterIdentityNamespace = "capg-system" + gcpClusterIdentitySecret = "gcp-cluster-identity-secret" + gcpFlagsCategory = "GCP authentication" +) + +var gcpClusterIdentitySecretType = corev1.SecretTypeOpaque + +type GCPClusterIdentityConfig struct { + *v1.SecretApplyConfiguration + ManifestFiles []string + ManifestFilesDir string +} + +func NewGCPClusterIdentityConfig(gcp *google_provider.GCPConfigure) *GCPClusterIdentityConfig { + gcpcc := &GCPClusterIdentityConfig{ + SecretApplyConfiguration: v1.Secret(gcpClusterIdentitySecret, gcpClusterIdentityNamespace), + ManifestFilesDir: filepath.Join(os.TempDir(), gcpClusterIdentityName), + } + + gcpcc.SecretApplyConfiguration.Type = &gcpClusterIdentitySecretType + gcpcc.SecretApplyConfiguration.Data = map[string][]byte{"credentials": gcp.AppCredentials.JSON()} + + return gcpcc +} + +func (gic *GCPClusterIdentityConfig) createGCPClusterIdentitySecretManifestFiles() error { + if err := os.MkdirAll(gic.ManifestFilesDir, 0775); err != nil { + return err + } + + fileSecret, err := createManifestFile(gic.SecretApplyConfiguration, gic.ManifestFilesDir, gcpClusterIdentitySecret) + if err != nil { + return err + } + + gic.ManifestFiles = append(gic.ManifestFiles, fileSecret) + + return nil +} + +func (cc *ClusterCommands) applyGCPClusterIdentitySecret() error { + var kubectlArgs = []string{"apply"} + + gcp := google_provider.NewGCPConfigure(cc.Ctx.Context, cc.Conf.GCPConfigure.AppCredentialsPath) + if err := gcp.ReadSACredentials(); err != nil { + return err + } + + gic := NewGCPClusterIdentityConfig(gcp) + if err := gic.createGCPClusterIdentitySecretManifestFiles(); err != nil { + return err + } + + for _, val := range gic.ManifestFiles { + kubectlArgs = append(kubectlArgs, "-f", val) + } + + cc.SpecCMD = cc.kubectl(kubectlArgs...) + if err := releaseRunner(cc).runCMD(); err != nil { + if err := os.RemoveAll(gic.ManifestFilesDir); err != nil { + return err + } + + return err + } + + return os.RemoveAll(gic.ManifestFilesDir) +} + +func (cc *ClusterCommands) getGCPClusterContext() ([]byte, error) { + return google_provider.NewGCPConfigure(cc.Ctx.Context, cc.Conf.GCPConfigure.AppCredentialsPath). + GetGCPClusterContext(cc.Conf.Name) +} + +func (cc *ClusterCommands) createGCPNATGateway() error { + return google_provider.NewGCPConfigure(cc.Ctx.Context, cc.Conf.GCPConfigure.AppCredentialsPath). + CreateGCPCloudNATGateway(cc.Conf.GCPRegion) +} + +func (cc *ClusterCommands) deleteGCPNATGateway() error { + return google_provider.NewGCPConfigure(cc.Ctx.Context, cc.Conf.GCPConfigure.AppCredentialsPath). + DeleteGCPCloudNATGateway(cc.Conf.GCPRegion) +} + +func (cc *ClusterCommands) createGCPSecrets() error { + gcp := google_provider.NewGCPConfigure(cc.Ctx.Context, cc.Conf.GCPConfigure.AppCredentialsPath) + + secrets, err := gcp.GetGCPSecrets(cc.Conf.Tenant) + if err != nil { + return err + } + + if len(secrets) > 0 { + return nil + } + + walkMatch, err := util.WalkMatch(cc.Conf.SopsAgeKeys, cc.Conf.Tenant+"*"+util.SopsAgeKeyExt) + if err != nil { + return err + } + + for _, val := range walkMatch { + file, err := os.ReadFile(val) + if err != nil { + return err + } + + keyName := strings.TrimSuffix(filepath.Base(val), util.SopsAgeKeyExt) + + if err := gcp.SetGCPSecret(cc.Conf.Tenant, cc.Conf.GCPRegion, keyName, file); err != nil { + return err + } + } + + return nil +} diff --git a/cmd/cluster_capz.go b/cmd/cluster_capz.go new file mode 100644 index 0000000..de1bae8 --- /dev/null +++ b/cmd/cluster_capz.go @@ -0,0 +1,186 @@ +package cmd + +import ( + "os" + "path/filepath" + "strings" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + v1 "k8s.io/client-go/applyconfigurations/core/v1" + + "rmk/providers/azure_provider" + "rmk/util" +) + +const ( + azureClusterIdentityName = "azure-cluster-identity" + azureClusterIdentityNamespace = "capz-system" + azureClusterIdentitySecret = "azure-cluster-identity-secret" + azureFlagsCategory = "Azure authentication" +) + +var azureClusterIdentitySecretType = corev1.SecretTypeOpaque + +type AzureClusterIdentityConfig struct { + *AzureClusterIdentity + *v1.SecretApplyConfiguration + ManifestFilesDir string + ManifestFiles []string +} + +type AzureClusterIdentity struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec AzureClusterIdentitySpec `json:"spec,omitempty"` +} + +type AzureClusterIdentitySpec struct { + AllowedNamespaces struct { + NamespaceList []string `json:"list"` + Selector *metav1.LabelSelector `json:"selector,omitempty"` + } `json:"allowedNamespaces,omitempty"` + ClientID string `json:"clientID"` + ClientSecret corev1.SecretReference `json:"clientSecret,omitempty"` + TenantID string `json:"tenantID"` + Type string `json:"type"` +} + +func NewAzureClusterIdentityConfig(ac *azure_provider.AzureConfigure) *AzureClusterIdentityConfig { + acic := &AzureClusterIdentityConfig{ + AzureClusterIdentity: &AzureClusterIdentity{ + TypeMeta: metav1.TypeMeta{ + Kind: "AzureClusterIdentity", + APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: azureClusterIdentityName, + Namespace: azureClusterIdentityNamespace, + Labels: map[string]string{"clusterctl.cluster.x-k8s.io/move-hierarchy": "true"}, + }, + Spec: AzureClusterIdentitySpec{ + AllowedNamespaces: struct { + NamespaceList []string `json:"list"` + Selector *metav1.LabelSelector `json:"selector,omitempty"` + }(struct { + NamespaceList []string + Selector *metav1.LabelSelector + }{ + NamespaceList: []string{azureClusterIdentityNamespace}, + }), + ClientID: ac.ClientID, + ClientSecret: corev1.SecretReference{ + Name: azureClusterIdentitySecret, + Namespace: azureClusterIdentityNamespace, + }, + TenantID: ac.TenantID, + Type: "ServicePrincipal", + }, + }, + SecretApplyConfiguration: v1.Secret(azureClusterIdentitySecret, azureClusterIdentityNamespace), + ManifestFilesDir: filepath.Join(os.TempDir(), azureClusterIdentityName), + } + + acic.SecretApplyConfiguration.Type = &azureClusterIdentitySecretType + acic.SecretApplyConfiguration.Data = map[string][]byte{"clientSecret": []byte(ac.ClientSecret)} + + return acic +} + +func (acic *AzureClusterIdentityConfig) createAzureClusterIdentityManifestFiles() error { + if err := os.MkdirAll(acic.ManifestFilesDir, 0775); err != nil { + return err + } + + fileCR, err := createManifestFile(acic.AzureClusterIdentity, acic.ManifestFilesDir, azureClusterIdentityName) + if err != nil { + return err + } + + acic.ManifestFiles = append(acic.ManifestFiles, fileCR) + + fileSecret, err := createManifestFile(acic.SecretApplyConfiguration, acic.ManifestFilesDir, azureClusterIdentitySecret) + if err != nil { + return err + } + + acic.ManifestFiles = append(acic.ManifestFiles, fileSecret) + + return nil +} + +func (cc *ClusterCommands) applyAzureClusterIdentity() error { + var kubectlArgs = []string{"apply"} + + ac := azure_provider.NewAzureConfigure() + if err := ac.ReadSPCredentials(cc.Conf.Name); err != nil { + return err + } + + acic := NewAzureClusterIdentityConfig(ac) + if err := acic.createAzureClusterIdentityManifestFiles(); err != nil { + return err + } + + for _, val := range acic.ManifestFiles { + kubectlArgs = append(kubectlArgs, "-f", val) + } + + cc.SpecCMD = cc.kubectl(kubectlArgs...) + if err := releaseRunner(cc).runCMD(); err != nil { + if err := os.RemoveAll(acic.ManifestFilesDir); err != nil { + return err + } + + return err + } + + return os.RemoveAll(acic.ManifestFilesDir) +} + +func (cc *ClusterCommands) getAzureClusterContext() ([]byte, error) { + ac := azure_provider.NewAzureConfigure() + + if err := ac.NewAzureClient(cc.Ctx.Context, cc.Conf.Name); err != nil { + return nil, err + } + + return ac.GetAzureClusterContext(cc.Conf.Tenant, cc.Conf.Name) +} + +func (cc *ClusterCommands) createAzureSecrets(ac *azure_provider.AzureConfigure) error { + if err := ac.NewAzureClient(cc.Ctx.Context, cc.Conf.Name); err != nil { + return err + } + + secrets, err := ac.GetAzureSecrets() + if err != nil { + return err + } + + if len(secrets) > 0 { + return nil + } + + walkMatch, err := util.WalkMatch(cc.Conf.SopsAgeKeys, cc.Conf.Tenant+"*"+util.SopsAgeKeyExt) + if err != nil { + return err + } + + for _, val := range walkMatch { + file, err := os.ReadFile(val) + if err != nil { + return err + } + + keyName := strings.TrimSuffix(filepath.Base(val), util.SopsAgeKeyExt) + value := string(file) + + if err := ac.SetAzureSecret(keyName, value); err != nil { + return err + } + } + + return nil +} diff --git a/commands/commands.go b/cmd/commands.go similarity index 74% rename from commands/commands.go rename to cmd/commands.go index 5c7c1b9..8a143c2 100644 --- a/commands/commands.go +++ b/cmd/commands.go @@ -1,4 +1,4 @@ -package commands +package cmd import ( "sort" @@ -7,16 +7,15 @@ import ( "github.com/urfave/cli/v2/altsrc" "go.uber.org/zap" - "rmk/aws_provider" "rmk/config" "rmk/git_handler" - "rmk/system" + "rmk/util" ) type Flags map[string][]cli.Flag func Commands() []*cli.Command { - conf := &config.Config{AwsConfigure: &aws_provider.AwsConfigure{}} + conf := &config.Config{} gitSpec := &git_handler.GitSpec{ DefaultBranches: []string{ git_handler.DefaultDevelop, @@ -25,11 +24,8 @@ func Commands() []*cli.Command { }, } flags := Flags{ - "clusterCRLogin": flagsClusterCRLogin(), "clusterK3DCreate": flagsClusterK3DCreate(), "clusterK3DImport": flagsClusterK3DImport(), - "clusterPlan": flagsClusterPlan(), - "clusterStateDelete": flagsClusterStateDelete(), "clusterSwitch": flagsClusterSwitch(), "config": flagsConfig(), "configList": flagsConfigList(), @@ -57,7 +53,7 @@ func Commands() []*cli.Command { { Name: "zsh", Usage: "View Zsh completion scripts", - Description: system.CompletionZshDescription, + Description: util.CompletionZshDescription, Aliases: []string{"z"}, Category: "completion", Action: completionAction(), @@ -84,7 +80,7 @@ func Commands() []*cli.Command { Before: readInputSourceWithContext(gitSpec, conf, flags["hidden"]), Flags: flags["hidden"], Category: "config", - BashComplete: system.ShellCompleteCustomOutput, + BashComplete: util.ShellCompleteCustomOutput, Action: configDeleteAction(conf), }, { @@ -93,7 +89,7 @@ func Commands() []*cli.Command { Aliases: []string{"l"}, Flags: flags["configList"], Category: "config", - BashComplete: system.ShellCompleteCustomOutput, + BashComplete: util.ShellCompleteCustomOutput, Action: configListAction(conf, gitSpec), }, { @@ -103,7 +99,7 @@ func Commands() []*cli.Command { Before: readInputSourceWithContext(gitSpec, conf, flags["hidden"]), Flags: flags["hidden"], Category: "config", - BashComplete: system.ShellCompleteCustomOutput, + BashComplete: util.ShellCompleteCustomOutput, Action: configViewAction(conf), }, }, @@ -113,51 +109,73 @@ func Commands() []*cli.Command { Usage: "Cluster management", Subcommands: []*cli.Command{ { - Name: "container-registry", - Usage: "Container registry management", + Name: "capi", + Usage: "CAPI cluster management", Aliases: []string{"c"}, Category: "cluster", Subcommands: []*cli.Command{ { - Name: "login", - Usage: "Log in to container registry", - Before: readInputSourceWithContext(gitSpec, conf, flags["clusterCRLogin"]), - Flags: flags["clusterCRLogin"], - Category: "container-registry", - BashComplete: system.ShellCompleteCustomOutput, - Action: containerRegistryAction(conf, DockerRunner.dockerLogin), + Name: "create", + Usage: "Create CAPI management cluster", + Aliases: []string{"c"}, + Before: readInputSourceWithContext(gitSpec, conf, flags["clusterK3DCreate"]), + Flags: flags["clusterK3DCreate"], + Category: "capi", + BashComplete: util.ShellCompleteCustomOutput, + Action: K3DCreateAction(conf), + After: CAPIInitAction(conf), + }, + { + Name: "delete", + Usage: "Delete CAPI management cluster", + Aliases: []string{"d"}, + Before: readInputSourceWithContext(gitSpec, conf, flags["hidden"]), + Flags: flags["hidden"], + Category: "capi", + BashComplete: util.ShellCompleteCustomOutput, + Action: K3DAction(conf, K3DRunner.createDeleteK3DCluster), + }, + { + Name: "destroy", + Usage: "Destroy K8S target (workload) cluster", + Before: readInputSourceWithContext(gitSpec, conf, flags["hidden"]), + Flags: flags["hidden"], + Category: "capi", + BashComplete: util.ShellCompleteCustomOutput, + Action: CAPIProvisionDestroyAction(conf), + }, + { + Name: "list", + Usage: "List CAPI management clusters", + Aliases: []string{"l"}, + Before: readInputSourceWithContext(gitSpec, conf, flags["hidden"]), + Flags: flags["hidden"], + Category: "capi", + BashComplete: util.ShellCompleteCustomOutput, + Action: K3DAction(conf, K3DRunner.listK3DClusters), }, { - Name: "logout", - Usage: "Log out from container registry", + Name: "provision", + Usage: "Provision K8S target (workload) cluster", + Aliases: []string{"p"}, Before: readInputSourceWithContext(gitSpec, conf, flags["hidden"]), Flags: flags["hidden"], - Category: "container-registry", - BashComplete: system.ShellCompleteCustomOutput, - Action: containerRegistryAction(conf, DockerRunner.dockerLogout), + Category: "capi", + BashComplete: util.ShellCompleteCustomOutput, + Action: CAPIProvisionDestroyAction(conf), + }, + { + Name: "update", + Usage: "Update CAPI management cluster", + Aliases: []string{"u"}, + Before: readInputSourceWithContext(gitSpec, conf, flags["hidden"]), + Flags: flags["hidden"], + Category: "capi", + BashComplete: util.ShellCompleteCustomOutput, + Action: CAPIUpdateAction(conf), }, }, }, - { - Name: "destroy", - Usage: "Destroy AWS cluster using Terraform", - Aliases: []string{"d"}, - Before: readInputSourceWithContext(gitSpec, conf, flags["hidden"]), - Flags: flags["hidden"], - Category: "cluster", - BashComplete: system.ShellCompleteCustomOutput, - Action: clusterDestroyAction(conf), - }, - { - Name: "list", - Usage: "List all Terraform available workspaces", - Aliases: []string{"l"}, - Before: readInputSourceWithContext(gitSpec, conf, flags["hidden"]), - Flags: flags["hidden"], - Category: "cluster", - BashComplete: system.ShellCompleteCustomOutput, - Action: clusterListAction(conf), - }, { Name: "k3d", Usage: "K3D cluster management", @@ -171,7 +189,7 @@ func Commands() []*cli.Command { Before: readInputSourceWithContext(gitSpec, conf, flags["clusterK3DCreate"]), Flags: flags["clusterK3DCreate"], Category: "k3d", - BashComplete: system.ShellCompleteCustomOutput, + BashComplete: util.ShellCompleteCustomOutput, Action: K3DCreateAction(conf), }, { @@ -181,7 +199,7 @@ func Commands() []*cli.Command { Before: readInputSourceWithContext(gitSpec, conf, flags["hidden"]), Flags: flags["hidden"], Category: "k3d", - BashComplete: system.ShellCompleteCustomOutput, + BashComplete: util.ShellCompleteCustomOutput, Action: K3DAction(conf, K3DRunner.createDeleteK3DCluster), }, { @@ -191,7 +209,7 @@ func Commands() []*cli.Command { Before: readInputSourceWithContext(gitSpec, conf, flags["clusterK3DImport"]), Flags: flags["clusterK3DImport"], Category: "k3d", - BashComplete: system.ShellCompleteCustomOutput, + BashComplete: util.ShellCompleteCustomOutput, Action: K3DAction(conf, K3DRunner.importImageToK3DCluster), }, { @@ -201,7 +219,7 @@ func Commands() []*cli.Command { Before: readInputSourceWithContext(gitSpec, conf, flags["hidden"]), Flags: flags["hidden"], Category: "k3d", - BashComplete: system.ShellCompleteCustomOutput, + BashComplete: util.ShellCompleteCustomOutput, Action: K3DAction(conf, K3DRunner.listK3DClusters), }, { @@ -210,7 +228,7 @@ func Commands() []*cli.Command { Aliases: []string{"s"}, Before: readInputSourceWithContext(gitSpec, conf, flags["hidden"]), Flags: flags["hidden"], - BashComplete: system.ShellCompleteCustomOutput, + BashComplete: util.ShellCompleteCustomOutput, Category: "k3d", Action: K3DAction(conf, K3DRunner.startStopK3DCluster), }, @@ -220,59 +238,11 @@ func Commands() []*cli.Command { Before: readInputSourceWithContext(gitSpec, conf, flags["hidden"]), Flags: flags["hidden"], Category: "k3d", - BashComplete: system.ShellCompleteCustomOutput, + BashComplete: util.ShellCompleteCustomOutput, Action: K3DAction(conf, K3DRunner.startStopK3DCluster), }, }, }, - { - Name: "provision", - Usage: "Provision AWS cluster using Terraform", - Aliases: []string{"p"}, - Before: readInputSourceWithContext(gitSpec, conf, flags["clusterPlan"]), - Flags: flags["clusterPlan"], - Category: "cluster", - BashComplete: system.ShellCompleteCustomOutput, - Action: clusterProvisionAction(conf), - }, - { - Name: "state", - Usage: "State cluster management using Terraform", - Aliases: []string{"t"}, - Category: "cluster", - Subcommands: []*cli.Command{ - { - Name: "delete", - Usage: "Delete resource from Terraform state", - Aliases: []string{"d"}, - Before: readInputSourceWithContext(gitSpec, conf, flags["clusterStateDelete"]), - Flags: flags["clusterStateDelete"], - Category: "state", - BashComplete: system.ShellCompleteCustomOutput, - Action: clusterStateAction(conf, StateRunner.clusterStateDelete), - }, - { - Name: "list", - Usage: "List resources from Terraform state", - Aliases: []string{"l"}, - Before: readInputSourceWithContext(gitSpec, conf, flags["hidden"]), - Flags: flags["hidden"], - Category: "state", - BashComplete: system.ShellCompleteCustomOutput, - Action: clusterStateAction(conf, StateRunner.clusterStateList), - }, - { - Name: "refresh", - Usage: "Update state file for AWS cluster using Terraform", - Aliases: []string{"r"}, - Before: readInputSourceWithContext(gitSpec, conf, flags["hidden"]), - Flags: flags["hidden"], - Category: "state", - BashComplete: system.ShellCompleteCustomOutput, - Action: clusterStateAction(conf, StateRunner.clusterStateRefresh), - }, - }, - }, { Name: "switch", Usage: "Switch Kubernetes context for tenant cluster", @@ -280,7 +250,7 @@ func Commands() []*cli.Command { Before: readInputSourceWithContext(gitSpec, conf, flags["clusterSwitch"]), Flags: flags["clusterSwitch"], Category: "cluster", - BashComplete: system.ShellCompleteCustomOutput, + BashComplete: util.ShellCompleteCustomOutput, Action: clusterSwitchAction(conf), }, }, @@ -309,7 +279,7 @@ func Commands() []*cli.Command { Before: readInputSourceWithContext(gitSpec, conf, flags["projectGenerate"]), Flags: flags["projectGenerate"], Category: "project", - BashComplete: system.ShellCompleteCustomOutput, + BashComplete: util.ShellCompleteCustomOutput, Action: projectGenerateAction(conf, gitSpec), }, { @@ -319,7 +289,7 @@ func Commands() []*cli.Command { Before: readInputSourceWithContext(gitSpec, conf, flags["projectUpdate"]), Flags: flags["projectUpdate"], Category: "project", - BashComplete: system.ShellCompleteCustomOutput, + BashComplete: util.ShellCompleteCustomOutput, Action: projectUpdateAction(conf, gitSpec), }, }, @@ -335,7 +305,7 @@ func Commands() []*cli.Command { Before: readInputSourceWithContext(gitSpec, conf, flags["releaseHelmfile"]), Flags: flags["releaseHelmfile"], Category: "release", - BashComplete: system.ShellCompleteCustomOutput, + BashComplete: util.ShellCompleteCustomOutput, Action: releaseHelmfileAction(conf), }, { @@ -343,9 +313,9 @@ func Commands() []*cli.Command { Usage: "Destroy releases", Aliases: []string{"d"}, Before: readInputSourceWithContext(gitSpec, conf, flags["releaseHelmfile"]), - Flags: flags["releaseHelmfileWithOutput"], + Flags: flags["releaseHelmfile"], Category: "release", - BashComplete: system.ShellCompleteCustomOutput, + BashComplete: util.ShellCompleteCustomOutput, Action: releaseHelmfileAction(conf), }, { @@ -355,7 +325,7 @@ func Commands() []*cli.Command { Before: readInputSourceWithContext(gitSpec, conf, flags["releaseHelmfileWithOutput"]), Flags: flags["releaseHelmfileWithOutput"], Category: "release", - BashComplete: system.ShellCompleteCustomOutput, + BashComplete: util.ShellCompleteCustomOutput, Action: releaseHelmfileAction(conf), }, { @@ -365,7 +335,7 @@ func Commands() []*cli.Command { Before: readInputSourceWithContext(gitSpec, conf, flags["releaseRollback"]), Flags: flags["releaseRollback"], Category: "release", - BashComplete: system.ShellCompleteCustomOutput, + BashComplete: util.ShellCompleteCustomOutput, Action: releaseRollbackAction(conf), }, { @@ -375,7 +345,7 @@ func Commands() []*cli.Command { Before: readInputSourceWithContext(gitSpec, conf, flags["releaseHelmfile"]), Flags: flags["releaseHelmfile"], Category: "release", - BashComplete: system.ShellCompleteCustomOutput, + BashComplete: util.ShellCompleteCustomOutput, Action: releaseHelmfileAction(conf), }, { @@ -385,7 +355,7 @@ func Commands() []*cli.Command { Before: readInputSourceWithContext(gitSpec, conf, flags["releaseHelmfile"]), Flags: flags["releaseHelmfile"], Category: "release", - BashComplete: system.ShellCompleteCustomOutput, + BashComplete: util.ShellCompleteCustomOutput, Action: releaseHelmfileAction(conf), }, { @@ -395,7 +365,7 @@ func Commands() []*cli.Command { Before: readInputSourceWithContext(gitSpec, conf, flags["releaseUpdate"]), Flags: flags["releaseUpdate"], Category: "release", - BashComplete: system.ShellCompleteCustomOutput, + BashComplete: util.ShellCompleteCustomOutput, Action: releaseUpdateAction(conf, gitSpec), }, }, @@ -417,7 +387,7 @@ func Commands() []*cli.Command { Before: readInputSourceWithContext(gitSpec, conf, flags["secretManager"]), Flags: flags["secretManager"], Category: "manager", - BashComplete: system.ShellCompleteCustomOutput, + BashComplete: util.ShellCompleteCustomOutput, Action: secretMgrEncryptDecryptAction(conf), }, { @@ -427,7 +397,7 @@ func Commands() []*cli.Command { Before: readInputSourceWithContext(gitSpec, conf, flags["secretManager"]), Flags: flags["secretManager"], Category: "manager", - BashComplete: system.ShellCompleteCustomOutput, + BashComplete: util.ShellCompleteCustomOutput, Action: secretMgrEncryptDecryptAction(conf), }, { @@ -437,7 +407,7 @@ func Commands() []*cli.Command { Before: readInputSourceWithContext(gitSpec, conf, flags["secretGenerate"]), Flags: flags["secretGenerate"], Category: "manager", - BashComplete: system.ShellCompleteCustomOutput, + BashComplete: util.ShellCompleteCustomOutput, Action: secretMgrGenerateAction(conf), }, }, @@ -455,7 +425,7 @@ func Commands() []*cli.Command { Before: readInputSourceWithContext(gitSpec, conf, flags["hidden"]), Flags: flags["hidden"], Category: "keys", - BashComplete: system.ShellCompleteCustomOutput, + BashComplete: util.ShellCompleteCustomOutput, Action: secretKeysCreateAction(conf), }, { @@ -465,7 +435,7 @@ func Commands() []*cli.Command { Before: readInputSourceWithContext(gitSpec, conf, flags["hidden"]), Flags: flags["hidden"], Category: "keys", - BashComplete: system.ShellCompleteCustomOutput, + BashComplete: util.ShellCompleteCustomOutput, Action: secretKeysDownloadAction(conf), }, { @@ -475,7 +445,7 @@ func Commands() []*cli.Command { Before: readInputSourceWithContext(gitSpec, conf, flags["hidden"]), Flags: flags["hidden"], Category: "keys", - BashComplete: system.ShellCompleteCustomOutput, + BashComplete: util.ShellCompleteCustomOutput, Action: secretKeysUploadAction(conf), }, }, @@ -487,7 +457,7 @@ func Commands() []*cli.Command { Before: readInputSourceWithContext(gitSpec, conf, flags["hidden"]), Flags: flags["hidden"], Category: "secret", - BashComplete: system.ShellCompleteCustomOutput, + BashComplete: util.ShellCompleteCustomOutput, Action: secretAction(conf, SecretRunner.helmSecretsEncrypt), }, { @@ -497,7 +467,7 @@ func Commands() []*cli.Command { Before: readInputSourceWithContext(gitSpec, conf, flags["hidden"]), Flags: flags["hidden"], Category: "secret", - BashComplete: system.ShellCompleteCustomOutput, + BashComplete: util.ShellCompleteCustomOutput, Action: secretAction(conf, SecretRunner.helmSecretsDecrypt), }, { @@ -507,7 +477,7 @@ func Commands() []*cli.Command { Before: readInputSourceWithContext(gitSpec, conf, flags["hidden"]), Flags: flags["hidden"], Category: "secret", - BashComplete: system.ShellCompleteCustomOutput, + BashComplete: util.ShellCompleteCustomOutput, Action: secretAction(conf, SecretRunner.helmSecretsView), }, { @@ -516,7 +486,7 @@ func Commands() []*cli.Command { Before: readInputSourceWithContext(gitSpec, conf, flags["hidden"]), Flags: flags["hidden"], Category: "secret", - BashComplete: system.ShellCompleteCustomOutput, + BashComplete: util.ShellCompleteCustomOutput, Action: secretAction(conf, SecretRunner.helmSecretsEdit), }, }, @@ -525,7 +495,7 @@ func Commands() []*cli.Command { Name: "update", Usage: "Update RMK CLI to a new version", Flags: flags["update"], - BashComplete: system.ShellCompleteCustomOutput, + BashComplete: util.ShellCompleteCustomOutput, Action: updateAction(), }, } @@ -537,7 +507,7 @@ func initInputSourceWithContext(gitSpec *git_handler.GitSpec, flags []cli.Flag) return err } - return inputSourceContext(ctx, flags, system.GetHomePath(system.RMKDir, system.RMKConfig, gitSpec.ID+".yaml")) + return inputSourceContext(ctx, flags, util.GetHomePath(util.RMKDir, util.RMKConfig, gitSpec.ID+".yaml")) } } @@ -547,9 +517,13 @@ func readInputSourceWithContext(gitSpec *git_handler.GitSpec, conf *config.Confi return err } - configPath := system.GetHomePath(system.RMKDir, system.RMKConfig, gitSpec.ID+".yaml") + if ctx.Command.Name == "generate" && !util.IsExists(util.GetPwdPath(util.TenantProjectFile), true) { + return nil + } + + configPath := util.GetHomePath(util.RMKDir, util.RMKConfig, gitSpec.ID+".yaml") if err := conf.ReadConfigFile(configPath); err != nil { - zap.S().Errorf(system.ConfigNotInitializedErrorText) + zap.S().Errorf(util.ConfigNotInitializedErrorText) return err } @@ -568,7 +542,7 @@ func inputSourceContext(ctx *cli.Context, flags []cli.Flag, configPath string) e return nil, err } - if system.IsExists(configPath, true) { + if util.IsExists(configPath, true) { return altsrc.NewYamlSourceFromFile(configPath) } else { return &altsrc.MapInputSource{}, nil diff --git a/cmd/config.go b/cmd/config.go new file mode 100644 index 0000000..555b9cd --- /dev/null +++ b/cmd/config.go @@ -0,0 +1,626 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "regexp" + "strconv" + "strings" + "time" + + "github.com/Masterminds/semver" + "github.com/urfave/cli/v2" + "go.uber.org/zap" + + "rmk/config" + "rmk/git_handler" + "rmk/providers/aws_provider" + "rmk/providers/azure_provider" + "rmk/providers/google_provider" + "rmk/util" +) + +type ConfigCommands struct { + *ReleaseCommands +} + +func newConfigCommands(conf *config.Config, ctx *cli.Context, workDir string) *ConfigCommands { + return &ConfigCommands{&ReleaseCommands{Conf: conf, Ctx: ctx, WorkDir: workDir}} +} + +func (c *ConfigCommands) helmPlugin() *util.SpecCMD { + return &util.SpecCMD{ + Args: []string{"plugin"}, + Command: "helm", + Dir: c.WorkDir, + Ctx: c.Ctx.Context, + DisableStdOut: true, + Debug: false, + } +} + +func (c *ConfigCommands) configAws(profile string) error { + ac := aws_provider.NewAwsConfigure(c.Ctx.Context, profile) + ac.ConfigSource = strings.Join(ac.AWSSharedConfigFile(profile), "") + ac.CredentialsSource = strings.Join(ac.AWSSharedCredentialsFile(profile), "") + + if !util.IsExists(c.Ctx.String("config"), true) && + util.IsExists(ac.ConfigSource, true) && + util.IsExists(ac.CredentialsSource, true) { + if err := os.RemoveAll(ac.ConfigSource); err != nil { + return err + } + + if err := os.RemoveAll(ac.CredentialsSource); err != nil { + return err + } + } + + if util.IsExists(ac.ConfigSource, true) { + if err := ac.ReadAWSConfigProfile(); err != nil { + return err + } + } + + if c.Ctx.IsSet("aws-access-key-id") && c.Ctx.IsSet("aws-secret-access-key") { + ac.AwsCredentialsProfile.AccessKeyID = c.Ctx.String("aws-access-key-id") + ac.AwsCredentialsProfile.SecretAccessKey = c.Ctx.String("aws-secret-access-key") + } else if c.Ctx.IsSet("aws-access-key-id") { + ac.AwsCredentialsProfile.AccessKeyID = c.Ctx.String("aws-access-key-id") + ac.AwsCredentialsProfile.SecretAccessKey = "" + } else if c.Ctx.IsSet("aws-secret-access-key") { + ac.AwsCredentialsProfile.AccessKeyID = "" + ac.AwsCredentialsProfile.SecretAccessKey = c.Ctx.String("aws-secret-access-key") + } + + if c.Ctx.IsSet("aws-region") { + ac.Region = c.Ctx.String("aws-region") + } + + if c.Ctx.IsSet("aws-session-token") { + ac.AwsCredentialsProfile.SessionToken = c.Ctx.String("aws-session-token") + } + + if err := ac.ValidateAWSCredentials(); err != nil { + return err + } else { + c.Conf.AwsConfigure = ac + } + + if err := ac.WriteAWSConfigProfile(); err != nil { + return err + } + + return nil +} + +func (c *ConfigCommands) configAwsMFA() error { + var tokenExpiration time.Time + currentTime := time.Now() + regularProfile := c.Conf.Profile + regularProfileConfigSource := c.Conf.ConfigSource + regularProfileCredentialsSource := c.Conf.CredentialsSource + + if len(c.Conf.AWSMFATokenExpiration) > 0 { + unixTime, err := strconv.ParseInt(c.Conf.AWSMFATokenExpiration, 10, 64) + if err != nil { + return err + } + + tokenExpiration = time.Unix(unixTime, 0) + } + + if len(c.Conf.AWSMFAProfile) > 0 { + c.Conf.AwsConfigure.Profile = c.Conf.AWSMFAProfile + } + + if err := c.Conf.GetAWSMFADevicesSerialNumbers(); err != nil { + return err + } + + timeDiff := time.Time{}.Add(tokenExpiration.Sub(currentTime)).Format("15:04:05") + + if len(c.Conf.MFADeviceSerialNumber) > 0 { + zap.S().Infof("MFA device SerialNumber: %s", c.Conf.MFADeviceSerialNumber) + } else { + if len(c.Conf.AWSMFAProfile) > 0 && len(c.Conf.AWSMFATokenExpiration) > 0 { + ac := aws_provider.NewAwsConfigure(c.Ctx.Context, c.Conf.AWSMFAProfile) + ac.ConfigSource = strings.Join(ac.AWSSharedConfigFile(c.Conf.AWSMFAProfile), "") + ac.CredentialsSource = strings.Join(ac.AWSSharedCredentialsFile(c.Conf.AWSMFAProfile), "") + if util.IsExists(ac.ConfigSource, true) { + if err := ac.ReadAWSConfigProfile(); err != nil { + return err + } + } + + ac.Profile = strings.TrimSuffix(regularProfile, "-mfa") + ac.ConfigSource = strings.TrimSuffix(regularProfileConfigSource, "-mfa") + ac.CredentialsSource = strings.TrimSuffix(regularProfileCredentialsSource, "-mfa") + + if err := ac.WriteAWSConfigProfile(); err != nil { + return err + } + + if err := os.RemoveAll(strings.Join(c.Conf.AWSSharedConfigFile(c.Conf.AWSMFAProfile), "")); err != nil { + return err + } + + if err := os.RemoveAll(strings.Join(c.Conf.AWSSharedCredentialsFile(c.Conf.AWSMFAProfile), "")); err != nil { + return err + } + + c.Conf.AWSMFAProfile = "" + c.Conf.AWSMFATokenExpiration = "" + c.Conf.AwsConfigure.Profile = ac.Profile + c.Conf.AwsConfigure.ConfigSource = ac.ConfigSource + c.Conf.AwsConfigure.CredentialsSource = ac.CredentialsSource + + return nil + } + } + + if currentTime.Before(tokenExpiration) { + zap.S().Infof("MFA remaining time for token validity: %s", timeDiff) + } + + if len(c.Conf.MFADeviceSerialNumber) > 0 && currentTime.After(tokenExpiration) { + if err := c.Conf.GetAWSMFASessionToken(); err != nil { + return err + } + + c.Conf.AWSMFAProfile = regularProfile + "-mfa" + c.Conf.AWSMFATokenExpiration = strconv.FormatInt(c.Conf.Expiration.Unix(), 10) + + acMFA := aws_provider.NewAwsConfigure(c.Ctx.Context, regularProfile+"-mfa") + acMFA.AwsCredentialsProfile.AccessKeyID = c.Conf.MFAProfileCredentials.AccessKeyID + acMFA.AwsCredentialsProfile.SecretAccessKey = c.Conf.MFAProfileCredentials.SecretAccessKey + acMFA.ConfigSource = regularProfileConfigSource + "-mfa" + acMFA.CredentialsSource = regularProfileCredentialsSource + "-mfa" + acMFA.Region = c.Conf.Region + + if err := acMFA.WriteAWSConfigProfile(); err != nil { + return err + } + + acRegular := aws_provider.NewAwsConfigure(c.Ctx.Context, regularProfile) + acRegular.AwsCredentialsProfile.AccessKeyID = c.Conf.MFAToken.AccessKeyID + acRegular.AwsCredentialsProfile.SecretAccessKey = c.Conf.MFAToken.SecretAccessKey + acRegular.AwsCredentialsProfile.SessionToken = c.Conf.MFAToken.SessionToken + acRegular.ConfigSource = regularProfileConfigSource + acRegular.CredentialsSource = regularProfileCredentialsSource + acRegular.Region = c.Conf.Region + + if err := acRegular.WriteAWSConfigProfile(); err != nil { + return err + } + } + + c.Conf.AwsConfigure.Profile = strings.TrimSuffix(regularProfile, "-mfa") + c.Conf.AwsConfigure.ConfigSource = strings.TrimSuffix(regularProfileConfigSource, "-mfa") + c.Conf.AwsConfigure.CredentialsSource = strings.TrimSuffix(regularProfileCredentialsSource, "-mfa") + + return nil +} + +func (c *ConfigCommands) installHelmPlugin(plugin config.Package, args ...string) error { + c.SpecCMD = c.helmPlugin() + c.SpecCMD.Args = append(c.SpecCMD.Args, args...) + if err := releaseRunner(c).runCMD(); err != nil { + if !strings.Contains(c.SpecCMD.StderrBuf.String(), util.HelmPluginExist) { + return fmt.Errorf("Helm plugin %s installation failed: \n%s", plugin.Name, c.SpecCMD.StderrBuf.String()) + } + } + + if !strings.Contains(c.SpecCMD.StderrBuf.String(), util.HelmPluginExist) { + zap.S().Infof("installing Helm plugin: %s", plugin.Name) + } + + return nil +} + +func (c *ConfigCommands) configHelmPlugins() error { + var ( + helmPluginsUpdate = make(map[string]*config.Package) + helmPluginsInstalled = make(map[string]*config.Package) + ) + + c.SpecCMD = c.helmPlugin() + c.SpecCMD.Args = append(c.SpecCMD.Args, "list") + + if err := releaseRunner(c).runCMD(); err != nil { + return fmt.Errorf("get Helm plugin list failed: %s", c.SpecCMD.StderrBuf.String()) + } + + for _, val := range strings.Split(c.SpecCMD.StdoutBuf.String(), "\n")[1:] { + reg, _ := regexp.Compile(`\s+`) + plugin := strings.Split(reg.ReplaceAllString(val, "|"), "|") + if len(plugin) > 1 { + helmPluginsInstalled[plugin[0]] = &config.Package{ + Name: plugin[0], + Version: plugin[1], + } + } + } + + for name, plugin := range c.Conf.HelmPlugins { + plSemVer, _ := semver.NewVersion(plugin.Version) + for _, pl := range helmPluginsInstalled { + plSV, _ := semver.NewVersion(pl.Version) + if pl.Name == plugin.Name && !plSemVer.Equal(plSV) { + helmPluginsUpdate[name] = plugin + break + } + } + } + + for _, plugin := range helmPluginsUpdate { + zap.S().Infof("Helm plugin %s detect new version %s from %s", plugin.Name, plugin.Version, util.TenantProjectFile) + c.SpecCMD = c.helmPlugin() + c.SpecCMD.Args = append(c.SpecCMD.Args, "uninstall", plugin.Name) + if err := releaseRunner(c).runCMD(); err != nil { + return fmt.Errorf("Helm plugin %s uninstallation failed: \n%s", + plugin.Name, c.SpecCMD.StderrBuf.String()) + } + + if err := c.installHelmPlugin(*plugin, "install", plugin.Url, "--version="+plugin.Version); err != nil { + return err + } + } + + for name, plugin := range c.Conf.HelmPlugins { + if _, ok := helmPluginsInstalled[name]; !ok { + if err := c.installHelmPlugin(*plugin, "install", plugin.Url, "--version="+plugin.Version); err != nil { + return err + } + } + } + + return nil +} + +func initAWSProfile(c *cli.Context, conf *config.Config, gitSpec *git_handler.GitSpec) error { + var profile string + + conf.AwsConfigure = aws_provider.NewAwsConfigure(c.Context, gitSpec.ID) + conf.AWSMFAProfile = c.String("aws-mfa-profile") + conf.AWSMFATokenExpiration = c.String("aws-mfa-token-expiration") + + // Detect if MFA is enabled + if len(conf.AWSMFAProfile) > 0 && len(conf.AWSMFATokenExpiration) > 0 { + profile = conf.AWSMFAProfile + } else { + profile = conf.Profile + } + + if err := newConfigCommands(conf, c, util.GetPwdPath("")).configAws(profile); err != nil { + return err + } + + if ok, err := conf.AwsConfigure.GetAwsConfigure(profile); err != nil && ok { + zap.S().Warnf("%s", err.Error()) + } else if !ok && err != nil { + return err + } + + if err := newConfigCommands(conf, c, util.GetPwdPath("")).configAwsMFA(); err != nil { + return err + } + + secrets, err := aws_provider.NewAwsConfigure(c.Context, conf.Profile).GetAWSSecrets(conf.Tenant) + if err != nil { + return err + } + + if len(secrets) == 0 { + zap.S().Warnf("SOPS Age keys contents for tenant %s not found in AWS Secrets Manager secrets", + conf.Tenant) + } + + for key, val := range secrets { + zap.S().Infof("download AWS Secrets Manager secret %s to %s", + key, filepath.Join(conf.SopsAgeKeys, key+util.SopsAgeKeyExt)) + if err := os.WriteFile(filepath.Join(conf.SopsAgeKeys, key+util.SopsAgeKeyExt), val, 0644); err != nil { + return err + } + } + + return nil +} + +func initAzureProfile(c *cli.Context, conf *config.Config, gitSpec *git_handler.GitSpec) error { + ac := azure_provider.NewAzureConfigure() + asp := azure_provider.NewRawSP() + + if util.IsExists( + util.GetHomePath(azure_provider.AzureHomeDir, azure_provider.AzurePrefix+gitSpec.ID+".json"), true) { + if err := ac.ReadSPCredentials(gitSpec.ID); err != nil { + return err + } + } + + if c.Bool("azure-service-principle") { + if err := json.NewDecoder(os.Stdin).Decode(&asp); err != nil { + return fmt.Errorf("unable to deserialize JSON from STDIN: %s", err.Error()) + } + + ac.MergeAzureRawSP(asp) + } + + if c.IsSet("azure-client-id") { + ac.ClientID = c.String("azure-client-id") + } + + if c.IsSet("azure-client-secret") { + ac.ClientSecret = c.String("azure-client-secret") + } + + if c.IsSet("azure-location") { + ac.Location = c.String("azure-location") + } + + if c.IsSet("azure-subscription-id") { + ac.SubscriptionID = c.String("azure-subscription-id") + } + + if c.IsSet("azure-tenant-id") { + ac.TenantID = c.String("azure-tenant-id") + } + + if err := ac.ValidateSPCredentials(); err != nil { + return err + } else { + conf.AzureConfigure = ac + } + + if err := ac.WriteSPCredentials(gitSpec.ID); err != nil { + return err + } + + if err := ac.NewAzureClient(c.Context, gitSpec.ID); err != nil { + return err + } + + ok, err := ac.GetAzureKeyVault(conf.Tenant) + if err != nil { + return err + } + + if !ok { + err := ac.CreateAzureKeyVault(conf.Tenant) + if err != nil { + return err + } + } else { + secrets, err := ac.GetAzureSecrets() + if err != nil { + return err + } + + if len(secrets) == 0 { + zap.S().Warnf("SOPS Age keys contents for tenant %s not found in Azure Key Vault secrets", + conf.Tenant) + } + + for key, val := range secrets { + zap.S().Infof("download Azure Key Vault secret %s to %s", + key, filepath.Join(conf.SopsAgeKeys, key+util.SopsAgeKeyExt)) + if err := os.WriteFile(filepath.Join(conf.SopsAgeKeys, key+util.SopsAgeKeyExt), val, 0644); err != nil { + return err + } + } + } + + return nil +} + +func initGCPProfile(c *cli.Context, conf *config.Config, gitSpec *git_handler.GitSpec) error { + gcp := google_provider.NewGCPConfigure(c.Context, + util.GetHomePath(google_provider.GoogleHomeDir, google_provider.GoogleCredentialsPrefix+gitSpec.ID+".json")) + + if c.IsSet("google-application-credentials") { + gcp.AppCredentialsPath = c.String("google-application-credentials") + if err := gcp.ReadSACredentials(); err != nil { + return err + } + + if err := gcp.CopySACredentials(gitSpec.ID); err != nil { + return err + } + } else { + if err := gcp.ReadSACredentials(); err != nil { + return err + } + } + + if !c.IsSet("gcp-region") { + return fmt.Errorf("GCP provider option gcp-region required") + } + + conf.GCPRegion = c.String("gcp-region") + conf.GCPConfigure = gcp + + secrets, err := gcp.GetGCPSecrets(conf.Tenant) + if err != nil { + return err + } + + if len(secrets) == 0 { + zap.S().Warnf("SOPS Age keys contents for tenant %s not found in GCP Secrets Manager secrets", + conf.Tenant) + } + + for key, val := range secrets { + zap.S().Infof("download GCP Secrets Manager secret %s to %s", + key, filepath.Join(conf.SopsAgeKeys, key+util.SopsAgeKeyExt)) + if err := os.WriteFile(filepath.Join(conf.SopsAgeKeys, key+util.SopsAgeKeyExt), val, 0644); err != nil { + return err + } + } + + return nil +} + +func configDeleteAction(conf *config.Config) cli.ActionFunc { + return func(c *cli.Context) error { + if err := util.ValidateNArg(c, 0); err != nil { + return err + } + + switch { + case c.String("cluster-provider") == aws_provider.AWSClusterProvider: + // Delete MFA profile + if len(conf.AWSMFAProfile) > 0 && len(conf.AWSMFATokenExpiration) > 0 { + if err := os.RemoveAll(strings.Join(conf.AWSSharedConfigFile(conf.AWSMFAProfile), "")); err != nil { + return err + } + + if err := os.RemoveAll(strings.Join(conf.AWSSharedCredentialsFile(conf.AWSMFAProfile), "")); err != nil { + return err + } + } + + // Delete config regular profile + if err := os.RemoveAll(strings.Join(conf.AWSSharedConfigFile(conf.Profile), "")); err != nil { + return err + } + + // Delete credentials regular profile + if err := os.RemoveAll(strings.Join(conf.AWSSharedCredentialsFile(conf.Profile), "")); err != nil { + return err + } + case c.String("cluster-provider") == azure_provider.AzureClusterProvider: + if err := os.RemoveAll(util.GetHomePath(azure_provider.AzureHomeDir, + azure_provider.AzurePrefix+conf.Name+".json")); err != nil { + return err + } + case c.String("cluster-provider") == google_provider.GoogleClusterProvider: + if err := os.RemoveAll(util.GetHomePath(google_provider.GoogleHomeDir, + google_provider.GoogleCredentialsPrefix+conf.Name+".json")); err != nil { + return err + } + } + + if err := os.RemoveAll(c.String("config")); err != nil { + return err + } + + zap.S().Infof("deleted config file by path: %s", c.String("config")) + + return nil + } +} + +func configInitAction(conf *config.Config, gitSpec *git_handler.GitSpec) cli.ActionFunc { + return func(c *cli.Context) error { + zap.S().Infof("loaded config file by path: %s", c.String("config")) + + start := time.Now() + + conf.Name = gitSpec.ID + conf.Tenant = gitSpec.RepoPrefixName + conf.Environment = gitSpec.DefaultBranch + conf.ClusterProvider = c.String("cluster-provider") + conf.ProgressBar = c.Bool("progress-bar") + conf.GitHubToken = c.String("github-token") + conf.SopsAgeKeys = util.GetHomePath(util.RMKDir, util.SopsRootName, conf.Tenant) + zap.S().Infof("RMK will use values for %s environment", conf.Environment) + + if c.Bool("slack-notifications") { + conf.SlackNotifications = c.Bool("slack-notifications") + if !c.IsSet("slack-webhook") || !c.IsSet("slack-channel") { + return fmt.Errorf("parameters --slack-webhook, --slack-channel not set, " + + "required if Slack notifications are enabled") + } else { + conf.SlackWebHook = c.String("slack-webhook") + conf.SlackChannel = c.String("slack-channel") + conf.SlackMsgDetails = c.StringSlice("slack-message-details") + } + } + + switch conf.ClusterProvider { + case aws_provider.AWSClusterProvider: + conf.AzureConfigure = nil + conf.GCPConfigure = nil + if err := initAWSProfile(c, conf, gitSpec); err != nil { + return err + } + case azure_provider.AzureClusterProvider: + conf.AwsConfigure = nil + conf.GCPConfigure = nil + if err := initAzureProfile(c, conf, gitSpec); err != nil { + return err + } + case google_provider.GoogleClusterProvider: + conf.AwsConfigure = nil + conf.AzureConfigure = nil + if err := initGCPProfile(c, conf, gitSpec); err != nil { + return err + } + case util.LocalClusterProvider: + conf.AwsConfigure = nil + conf.AzureConfigure = nil + conf.GCPConfigure = nil + } + + if err := conf.InitConfig().SetRootDomain(c, gitSpec.ID); err != nil { + return err + } + + if err := conf.CreateConfigFile(); err != nil { + return err + } + + if err := resolveDependencies(conf.InitConfig(), c, false); err != nil { + return err + } + + zap.S().Infof("time spent on initialization: %.fs", time.Since(start).Seconds()) + + return nil + } +} + +func configListAction(conf *config.Config, gitSpec *git_handler.GitSpec) cli.ActionFunc { + return func(c *cli.Context) error { + if err := util.ValidateNArg(c, 0); err != nil { + return err + } + + if err := gitSpec.GenerateID(); err != nil { + return err + } + + conf.Tenant = gitSpec.RepoPrefixName + return conf.GetConfigs(c.Bool("all")) + } +} + +func configViewAction(conf *config.Config) cli.ActionFunc { + return func(c *cli.Context) error { + if err := util.ValidateNArg(c, 0); err != nil { + return err + } + + if c.String("log-format") == "json" { + serializeJsonConfig, err := conf.SerializeJsonConfig() + if err != nil { + return err + } + + zap.L().Info("RMK", zap.Any("config", json.RawMessage(serializeJsonConfig))) + return nil + } + + serializeConfig, err := conf.SerializeConfig() + if err != nil { + return err + } + + zap.S().Infof("loaded config file by path: %s", c.String("config")) + fmt.Printf("%s\n", string(serializeConfig)) + + return nil + } +} diff --git a/commands/flags.go b/cmd/flags.go similarity index 65% rename from commands/flags.go rename to cmd/flags.go index b4f385d..4d413e1 100644 --- a/commands/flags.go +++ b/cmd/flags.go @@ -1,51 +1,42 @@ -package commands +package cmd import ( "github.com/urfave/cli/v2" "github.com/urfave/cli/v2/altsrc" - "rmk/git_handler" - "rmk/system" + "rmk/util" ) func flagsConfig() []cli.Flag { return []cli.Flag{ - altsrc.NewStringFlag( - &cli.StringFlag{ - Name: "artifact-mode", - Usage: "choice of artifact usage model, available: none, online", - Aliases: []string{"am"}, - EnvVars: []string{"RMK_ARTIFACT_MODE"}, - Value: system.ArtifactModeDefault, - }, - ), - altsrc.NewStringFlag( - &cli.StringFlag{ - Name: "aws-ecr-host", - Usage: "AWS ECR host", - Aliases: []string{"aeh"}, - EnvVars: []string{"RMK_AWS_ECR_HOST"}, - Value: system.AWSECRHost, - }, - ), - altsrc.NewStringFlag( - &cli.StringFlag{ - Name: "aws-ecr-region", - Usage: "AWS region for specific ECR host", - Aliases: []string{"aer"}, - EnvVars: []string{"RMK_AWS_ECR_REGION"}, - Value: system.AWSECRRegion, - }, - ), - altsrc.NewStringFlag( - &cli.StringFlag{ - Name: "aws-ecr-user-name", - Usage: "AWS ECR user name", - Aliases: []string{"aeun"}, - EnvVars: []string{"RMK_AWS_ECR_USER_NAME"}, - Value: system.AWSECRUserName, - }, - ), + &cli.StringFlag{ + Category: awsFlagsCategory, + Name: "aws-access-key-id", + Usage: "AWS access key ID for IAM user", + Aliases: []string{"awid"}, + EnvVars: []string{"RMK_AWS_ACCESS_KEY_ID", "AWS_ACCESS_KEY_ID"}, + }, + &cli.StringFlag{ + Category: awsFlagsCategory, + Name: "aws-region", + Usage: "AWS region for current AWS account", + Aliases: []string{"awr"}, + EnvVars: []string{"RMK_AWS_REGION", "AWS_REGION", "AWS_DEFAULT_REGION"}, + }, + &cli.StringFlag{ + Category: awsFlagsCategory, + Name: "aws-secret-access-key", + Usage: "AWS secret access key for IAM user", + Aliases: []string{"awsk"}, + EnvVars: []string{"RMK_AWS_SECRET_ACCESS_KEY", "AWS_SECRET_ACCESS_KEY"}, + }, + &cli.StringFlag{ + Category: awsFlagsCategory, + Name: "aws-session-token", + Usage: "AWS session token for IAM user", + Aliases: []string{"awst"}, + EnvVars: []string{"RMK_AWS_SESSION_TOKEN", "AWS_SESSION_TOKEN"}, + }, altsrc.NewStringFlag( &cli.StringFlag{ Name: "aws-mfa-profile", @@ -58,65 +49,84 @@ func flagsConfig() []cli.Flag { Hidden: true, }, ), - &cli.BoolFlag{ - Name: "aws-reconfigure", - Usage: "force AWS profile creation", - Aliases: []string{"r"}, + &cli.StringFlag{ + Category: azureFlagsCategory, + Name: "azure-client-id", + Usage: "Azure client ID for Service Principal", + Aliases: []string{"azid"}, + EnvVars: []string{"RMK_AZURE_CLIENT_ID", "AZURE_CLIENT_ID"}, + }, + &cli.StringFlag{ + Category: azureFlagsCategory, + Name: "azure-client-secret", + Usage: "Azure client secret for Service Principal", + Aliases: []string{"azp"}, + EnvVars: []string{"RMK_AZURE_CLIENT_SECRET", "AZURE_CLIENT_SECRET"}, + }, + &cli.StringFlag{ + Category: azureFlagsCategory, + Name: "azure-location", + Usage: "Azure location", + Aliases: []string{"azl"}, + EnvVars: []string{"RMK_AZURE_LOCATION", "AZURE_LOCATION"}, }, &cli.BoolFlag{ - Name: "aws-reconfigure-artifact-license", - Usage: "force AWS profile creation for artifact license, used only if RMK config option artifact-mode has values: online, offline", - Aliases: []string{"l"}, + Category: azureFlagsCategory, + Name: "azure-service-principle", + Usage: "Azure service principal STDIN content", + Aliases: []string{"azsp"}, }, - altsrc.NewBoolFlag( - &cli.BoolFlag{ - Name: "cluster-provisioner-state-locking", - Usage: "disable or enable cluster provisioner state locking", - Aliases: []string{"c"}, - Value: true, - }, - ), &cli.StringFlag{ - Name: "config", - Hidden: true, + Category: azureFlagsCategory, + Name: "azure-subscription-id", + Usage: "Azure subscription ID for current platform domain", + Aliases: []string{"azs"}, + EnvVars: []string{"RMK_AZURE_SUBSCRIPTION_ID", "AZURE_SUBSCRIPTION_ID"}, + }, + &cli.StringFlag{ + Category: azureFlagsCategory, + Name: "azure-tenant-id", + Usage: "Azure tenant ID for Service Principal", + Aliases: []string{"azt"}, + EnvVars: []string{"RMK_AZURE_TENANT_ID", "AZURE_TENANT_ID"}, }, altsrc.NewStringFlag( &cli.StringFlag{ - Name: "config-from", - Hidden: true, + Name: "cluster-provider", + Usage: "cluster provider for provisioning", + Aliases: []string{"cp"}, + EnvVars: []string{"RMK_CLUSTER_PROVIDER"}, + Value: util.LocalClusterProvider, }, ), &cli.StringFlag{ - Name: "config-from-environment", - Usage: "inheritance of RMK config credentials from environments: " + - git_handler.DefaultDevelop + ", " + git_handler.DefaultStaging + ", " + git_handler.DefaultProduction, - Aliases: []string{"cfe"}, - EnvVars: []string{"RMK_CONFIG_FROM_ENVIRONMENT"}, + Name: "config", + Hidden: true, }, altsrc.NewStringFlag( &cli.StringFlag{ Name: "github-token", - Usage: "personal access token for download GitHub artifacts", + Usage: "GitHub personal access token, required when using private repositories", Aliases: []string{"ght"}, EnvVars: []string{"RMK_GITHUB_TOKEN"}, }, ), altsrc.NewStringFlag( &cli.StringFlag{ - Name: "cloudflare-token", - Usage: "Cloudflare API token for provision NS records", - Aliases: []string{"cft"}, - EnvVars: []string{"RMK_CLOUDFLARE_TOKEN"}, - }, - ), - altsrc.NewStringFlag( - &cli.StringFlag{ - Name: "root-domain", - Usage: "domain name for external access to app services via ingress controller", - Aliases: []string{"rd"}, - EnvVars: []string{"RMK_ROOT_DOMAIN"}, + Category: gcpFlagsCategory, + Name: "gcp-region", + Usage: "GCP region", + Aliases: []string{"gr"}, + EnvVars: []string{"RMK_GCP_REGION", "GCP_REGION"}, }, ), + &cli.StringFlag{ + Category: gcpFlagsCategory, + Name: "google-application-credentials", + Usage: "path to GCP service account credentials JSON file", + Aliases: []string{"gac"}, + EnvVars: []string{"RMK_GOOGLE_APPLICATION_CREDENTIALS", "GOOGLE_APPLICATION_CREDENTIALS"}, + }, altsrc.NewBoolFlag( &cli.BoolFlag{ Name: "progress-bar", @@ -125,53 +135,39 @@ func flagsConfig() []cli.Flag { Value: true, }, ), - altsrc.NewStringFlag( - &cli.StringFlag{ - Name: "s3-charts-repo-region", - Usage: "location constraint region of S3 charts repo", - Aliases: []string{"scrr"}, - EnvVars: []string{"RMK_S3_CHARTS_REPO_REGION"}, - Value: system.S3ChartsRepoRegion, - }, - ), - altsrc.NewStringFlag( - &cli.StringFlag{ - Name: "cluster-provider", - Usage: "select cluster provider to provision clusters", - Aliases: []string{"cp"}, - EnvVars: []string{"RMK_CLUSTER_PROVIDER"}, - Value: system.AWSClusterProvider, - }, - ), altsrc.NewBoolFlag( &cli.BoolFlag{ - Name: "slack-notifications", - Usage: "enable Slack notifications", - Aliases: []string{"n"}, + Category: "Slack notifications", + Name: "slack-notifications", + Usage: "enable Slack notifications", + Aliases: []string{"n"}, }, ), altsrc.NewStringFlag( &cli.StringFlag{ - Name: "slack-webhook", - Usage: "URL for Slack webhook", - Aliases: []string{"sw"}, - EnvVars: []string{"RMK_SLACK_WEBHOOK"}, + Category: "Slack notifications", + Name: "slack-webhook", + Usage: "URL for Slack webhook", + Aliases: []string{"sw"}, + EnvVars: []string{"RMK_SLACK_WEBHOOK"}, }, ), altsrc.NewStringFlag( &cli.StringFlag{ - Name: "slack-channel", - Usage: "channel name for Slack notification", - Aliases: []string{"sc"}, - EnvVars: []string{"RMK_SLACK_CHANNEL"}, + Category: "Slack notifications", + Name: "slack-channel", + Usage: "channel name for Slack notifications", + Aliases: []string{"sc"}, + EnvVars: []string{"RMK_SLACK_CHANNEL"}, }, ), altsrc.NewStringSliceFlag( &cli.StringSliceFlag{ - Name: "slack-message-details", - Usage: "additional information for body of Slack message", - Aliases: []string{"smd"}, - EnvVars: []string{"RMK_SLACK_MESSAGE_DETAILS"}, + Category: "Slack notifications", + Name: "slack-message-details", + Usage: "additional information for body of Slack messages", + Aliases: []string{"smd"}, + EnvVars: []string{"RMK_SLACK_MESSAGE_DETAILS"}, }, ), } @@ -211,37 +207,6 @@ func flagsClusterK3DImport() []cli.Flag { ) } -func flagsClusterCRLogin() []cli.Flag { - return append(flagsHidden(), - &cli.BoolFlag{ - Name: "get-token", - Usage: "get ECR token for authentication", - Aliases: []string{"g"}, - }, - ) -} - -func flagsClusterPlan() []cli.Flag { - return append(flagsHidden(), - &cli.BoolFlag{ - Name: "plan", - Usage: "creates an execution Terraform plan", - Aliases: []string{"p"}, - }, - ) -} - -func flagsClusterStateDelete() []cli.Flag { - return append(flagsHidden(), - &cli.StringFlag{ - Name: "resource-address", - Usage: "resource address for delete from Terraform state", - Aliases: []string{"ra"}, - EnvVars: []string{"RMK_CLUSTER_STATE_RESOURCE_ADDRESS"}, - }, - ) -} - func flagsClusterSwitch() []cli.Flag { return append(flagsHidden(), &cli.BoolFlag{ @@ -259,6 +224,21 @@ func flagsProjectGenerate() []cli.Flag { Usage: "create SOPS age keys for generated project structure", Aliases: []string{"c"}, }, + &cli.StringSliceFlag{ + Name: "environments", + Usage: "list project environments. Root domain can take form of .root-domain=", + Aliases: []string{"e"}, + }, + &cli.StringSliceFlag{ + Name: "owners", + Usage: "list project owners", + Aliases: []string{"o"}, + }, + &cli.StringSliceFlag{ + Name: "scopes", + Usage: "list project scopes", + Aliases: []string{"s"}, + }, ) } @@ -441,24 +421,12 @@ func flagsHidden() []cli.Flag { Name: "config", Hidden: true, }, - altsrc.NewStringFlag( - &cli.StringFlag{ - Name: "artifact-mode", - Hidden: true, - }, - ), altsrc.NewStringFlag( &cli.StringFlag{ Name: "github-token", Hidden: true, }, ), - altsrc.NewStringFlag( - &cli.StringFlag{ - Name: "cloudflare-token", - Hidden: true, - }, - ), altsrc.NewStringFlag( &cli.StringFlag{ Name: "cluster-provider", diff --git a/cmd/k3d.go b/cmd/k3d.go new file mode 100644 index 0000000..e7fa295 --- /dev/null +++ b/cmd/k3d.go @@ -0,0 +1,182 @@ +package cmd + +import ( + "fmt" + "os" + "strings" + + "github.com/urfave/cli/v2" + + "rmk/config" + "rmk/util" +) + +type K3DRunner interface { + createDeleteK3DCluster() error + importImageToK3DCluster() error + listK3DClusters() error + startStopK3DCluster() error +} + +type K3DCommands struct { + *ReleaseCommands +} + +func newK3DCommands(conf *config.Config, ctx *cli.Context, workDir string) *K3DCommands { + return &K3DCommands{&ReleaseCommands{Conf: conf, Ctx: ctx, WorkDir: workDir}} +} + +func (k *K3DCommands) k3d(args ...string) *util.SpecCMD { + return &util.SpecCMD{ + Args: append([]string{}, args...), + Command: "k3d", + Dir: k.WorkDir, + Ctx: k.Ctx.Context, + DisableStdOut: false, + Debug: false, + } +} + +func (k *K3DCommands) prepareK3D(args ...string) error { + k.SpecCMD = k.k3d(args...) + k.SpecCMD.Debug = true + + switch { + case k.APICluster: + k.SpecCMD.Envs = append(k.SpecCMD.Envs, "K3D_NAME="+util.CAPI) + case k.K3DCluster: + k.SpecCMD.Envs = append(k.SpecCMD.Envs, "K3D_NAME="+k.Conf.Name) + } + + if len(k.Ctx.String("k3d-volume-host-path")) > 0 { + k.SpecCMD.Envs = append(k.SpecCMD.Envs, "K3D_VOLUME_HOST_PATH="+k.Ctx.String("k3d-volume-host-path")) + return nil + } + + k.SpecCMD.Envs = append(k.SpecCMD.Envs, "K3D_VOLUME_HOST_PATH="+util.GetPwdPath("")) + + return nil +} + +func (k *K3DCommands) selectCluster() { + switch k.Ctx.Command.Category { + case util.CAPI: + k.APICluster = true + case util.K3DPrefix: + k.K3DCluster = true + } +} + +func (k *K3DCommands) createDeleteK3DCluster() error { + k.selectCluster() + + if _, _, err := clusterRunner(&ClusterCommands{k.ReleaseCommands}).getKubeContext(); err != nil { + return err + } + + k.SpecCMD = k.prepareHelmfile("--log-level", "error", "-l", "cluster="+k.Ctx.Command.Category, "template") + k.SpecCMD.DisableStdOut = true + if err := releaseRunner(k).runCMD(); err != nil { + return fmt.Errorf("Helmfile failed to render template by label release: cluster=%s\n%s", + k.Ctx.Command.Category, k.SpecCMD.StderrBuf.String()) + } + + k3dConfig, err := util.CreateTempYAMLFile(os.TempDir(), k.Ctx.Command.Category+"-config", k.SpecCMD.StdoutBuf.Bytes()) + if err != nil { + return err + } + + if err := k.prepareK3D("cluster", k.Ctx.Command.Name, "--config", k3dConfig); err != nil { + return err + } + + if err := releaseRunner(k).runCMD(); err != nil { + if err := os.RemoveAll(k3dConfig); err != nil { + return err + } + + return err + } + + return os.RemoveAll(k3dConfig) +} + +func (k *K3DCommands) importImageToK3DCluster() error { + if err := k.prepareK3D(append(append([]string{}, "image", "import", "--cluster", k.Conf.Name, "--keep-tools"), + k.Ctx.StringSlice("k3d-import-image")...)...); err != nil { + return err + } + + return releaseRunner(k).runCMD() +} + +func (k *K3DCommands) listK3DClusters() error { + k.selectCluster() + + if _, _, err := clusterRunner(&ClusterCommands{k.ReleaseCommands}).getKubeContext(); err != nil { + return err + } + + if k.APICluster { + if err := k.prepareK3D("cluster", k.Ctx.Command.Name, util.CAPI); err != nil { + return err + } + + k.SpecCMD.DisableStdOut = true + if err := releaseRunner(k).runCMD(); err != nil { + if strings.Contains(k.SpecCMD.StderrBuf.String(), "No nodes found for given cluster") { + return fmt.Errorf("%s management cluster not found", strings.ToUpper(util.CAPI)) + } else { + return fmt.Errorf("%s", k.SpecCMD.StderrBuf.String()) + } + } + + fmt.Printf("%s", k.SpecCMD.StdoutBuf.String()) + + return nil + } + + if err := k.prepareK3D("cluster", k.Ctx.Command.Name); err != nil { + return err + } + + return releaseRunner(k).runCMD() +} + +func (k *K3DCommands) startStopK3DCluster() error { + k.selectCluster() + + if _, _, err := clusterRunner(&ClusterCommands{k.ReleaseCommands}).getKubeContext(); err != nil { + return err + } + + if err := k.prepareK3D("cluster", k.Ctx.Command.Name, k.Conf.Name); err != nil { + return err + } + + return releaseRunner(k).runCMD() +} + +func K3DCreateAction(conf *config.Config) cli.ActionFunc { + return func(c *cli.Context) error { + if err := util.ValidateNArg(c, 0); err != nil { + return err + } + + if err := resolveDependencies(conf.InitConfig(), c, false); err != nil { + return err + } + + return newK3DCommands(conf, c, util.GetPwdPath("")).createDeleteK3DCluster() + } +} + +func K3DAction(conf *config.Config, action func(k3dRunner K3DRunner) error) cli.ActionFunc { + return func(c *cli.Context) error { + if err := util.ValidateNArg(c, 0); err != nil { + return err + } + + return action(newK3DCommands(conf, c, util.GetPwdPath(""))) + } +} diff --git a/commands/project_category.go b/cmd/project.go similarity index 66% rename from commands/project_category.go rename to cmd/project.go index 840cb69..9dd8dc3 100644 --- a/commands/project_category.go +++ b/cmd/project.go @@ -1,4 +1,4 @@ -package commands +package cmd import ( "bytes" @@ -11,6 +11,7 @@ import ( "strings" "text/template" + "github.com/google/go-cmp/cmp" "github.com/urfave/cli/v2" "go.uber.org/zap" "gopkg.in/yaml.v3" @@ -18,7 +19,7 @@ import ( "rmk/config" "rmk/git_handler" "rmk/notification" - "rmk/system" + "rmk/util" ) type ProjectCommands struct { @@ -55,7 +56,15 @@ type projectSpec struct { owners string } -func newProjectCommand(conf *config.Config, ctx *cli.Context, workDir string) *ProjectCommands { +func newProjectCommand(conf *config.Config, ctx *cli.Context, workDir string, gitSpec *git_handler.GitSpec) *ProjectCommands { + emptyConfig := &config.Config{} + if cmp.Equal(conf, emptyConfig) { + conf.Name = gitSpec.ID + conf.Tenant = gitSpec.RepoPrefixName + conf.Environment = gitSpec.DefaultBranch + conf.SopsAgeKeys = util.GetHomePath(util.RMKDir, util.SopsRootName, conf.Tenant) + } + return &ProjectCommands{ &parseContent{ TenantName: conf.Tenant, @@ -67,12 +76,64 @@ func newProjectCommand(conf *config.Config, ctx *cli.Context, workDir string) *P } } +func (p *ProjectCommands) createProjectFile() error { + var buf bytes.Buffer + + if !p.Ctx.IsSet("environments") || !p.Ctx.IsSet("scopes") { + return fmt.Errorf("%s file not found or values not set for flags: %s, %s", + util.GetPwdPath(util.TenantProjectFile), "environments", "scopes") + } + + if p.Ctx.IsSet("environments") { + p.projectFile.Spec.Environments = make(map[string]*config.ProjectRootDomain) + for _, val := range p.Ctx.StringSlice("environments") { + if len(val) > 0 { + matchRootDomain := regexp.MustCompile(`^.+\.root-domain=.+$`).MatchString(val) + splitRootDomain := strings.SplitN(val, ".", 2) + + if !matchRootDomain && len(splitRootDomain) == 2 { + return fmt.Errorf("option %s for environment %s not set correctly", + splitRootDomain[1], splitRootDomain[0]) + } + + if matchRootDomain && len(splitRootDomain) == 2 { + p.projectFile.Spec.Environments[splitRootDomain[0]] = &config.ProjectRootDomain{ + RootDomain: strings.TrimPrefix(splitRootDomain[1], "root-domain="), + } + } + + if !matchRootDomain || len(splitRootDomain) == 1 { + p.projectFile.Spec.Environments[splitRootDomain[0]] = &config.ProjectRootDomain{} + } + } + } + } + + if p.Ctx.IsSet("owners") { + p.projectFile.Spec.Owners = p.Ctx.StringSlice("owners") + } + + if p.Ctx.IsSet("scopes") { + p.projectFile.Spec.Scopes = p.Ctx.StringSlice("scopes") + } + + encoder := yaml.NewEncoder(&buf) + encoder.SetIndent(2) + if err := encoder.Encode(&p.projectFile); err != nil { + return err + } + + return os.WriteFile(util.GetPwdPath(util.TenantProjectFile), buf.Bytes(), 0644) +} + func (p *ProjectCommands) readProjectFile() error { - if !system.IsExists(system.GetPwdPath(system.TenantProjectFile), true) { - return fmt.Errorf("%s file not found", system.GetPwdPath(system.TenantProjectFile)) + if !util.IsExists(util.GetPwdPath(util.TenantProjectFile), true) { + if err := p.createProjectFile(); err != nil { + return err + } } - data, err := os.ReadFile(system.GetPwdPath(system.TenantProjectFile)) + data, err := os.ReadFile(util.GetPwdPath(util.TenantProjectFile)) if err != nil { return err } @@ -97,7 +158,7 @@ func (p *ProjectCommands) serializeProjectFile() ([]byte, error) { count++ zap.S().Infof("version changed for dependency %s, affected file: %s", pkg.Name, - system.GetPwdPath(system.TenantProjectFile)) + util.GetPwdPath(util.TenantProjectFile)) break } } @@ -137,13 +198,13 @@ func (p *ProjectCommands) updateProjectFile(gitSpec *git_handler.GitSpec) error } if data != nil { - if err := os.WriteFile(system.GetPwdPath(system.TenantProjectFile), data, 0644); err != nil { + if err := os.WriteFile(util.GetPwdPath(util.TenantProjectFile), data, 0644); err != nil { return err } if !p.Ctx.Bool("skip-commit") { if err := gitSpec.GitCommitPush( - system.GetPwdPath(system.TenantProjectFile), + util.GetPwdPath(util.TenantProjectFile), p.genMsgCommit(), p.Conf.GitHubToken); err != nil { return err } @@ -151,7 +212,7 @@ func (p *ProjectCommands) updateProjectFile(gitSpec *git_handler.GitSpec) error tmp := ¬ification.TmpUpdate{Config: p.Conf, Context: p.Ctx} tmp.ChangesList = append(tmp.ChangesList, p.Ctx.String("dependency")) - tmp.PathToFile = system.TenantProjectFile + tmp.PathToFile = util.TenantProjectFile if err := notification.SlackInit(tmp, notification.SlackTmp(tmp).TmpProjectUpdateMsg()).SlackDeclareNotify(); err != nil { return err @@ -162,7 +223,7 @@ func (p *ProjectCommands) updateProjectFile(gitSpec *git_handler.GitSpec) error } func (p *ProjectCommands) writeProjectFiles(path, data string) error { - if system.IsExists(path, true) { + if util.IsExists(path, true) { zap.S().Warnf("file %s already exists", path) return nil } @@ -188,7 +249,7 @@ func (p *ProjectCommands) generateReadme(gitSpec *git_handler.GitSpec) error { return err } - if err := p.writeProjectFiles(system.GetPwdPath(system.ReadmeFileName), readmeF); err != nil { + if err := p.writeProjectFiles(util.GetPwdPath(util.ReadmeFileName), readmeF); err != nil { return err } @@ -196,9 +257,15 @@ func (p *ProjectCommands) generateReadme(gitSpec *git_handler.GitSpec) error { } func (p *ProjectCommands) generateHelmfile() error { - sort.Strings(p.projectFile.Spec.Environments) + var environmentKeys = make([]string, 0, len(p.projectFile.Spec.Environments)) - for key, name := range p.projectFile.Spec.Environments { + for key := range p.projectFile.Spec.Environments { + environmentKeys = append(environmentKeys, key) + } + + sort.Strings(environmentKeys) + + for key, name := range environmentKeys { p.EnvironmentName = name hEnvironments, err := p.Conf.ParseTemplate(template.New("Helmfile"), &p.parseContent, helmfileEnvironments) if err != nil { @@ -235,7 +302,7 @@ func (p *ProjectCommands) generateHelmfile() error { p.HelmfileParts = append(p.HelmfileParts, hReleases) - if err := p.writeProjectFiles(system.GetPwdPath(system.HelmfileGoTmplName), strings.Join(p.HelmfileParts, "\n")); err != nil { + if err := p.writeProjectFiles(util.GetPwdPath(util.HelmfileGoTmplName), strings.Join(p.HelmfileParts, "\n")); err != nil { return err } @@ -246,14 +313,6 @@ func (p *ProjectCommands) generateProjectFiles(gitSpec *git_handler.GitSpec) err for _, sc := range p.scopes { for _, env := range sc.environments { switch sc.name { - case "clusters": - if err := p.writeProjectFiles(filepath.Join(env.valuesPath, system.TerraformVarsFile), clusterVariables); err != nil { - return err - } - - if err := p.writeProjectFiles(filepath.Join(env.valuesPath, system.TerraformWGFile), clusterWorkerGroups); err != nil { - return err - } case p.TenantName: tGlobals, err := p.Conf.ParseTemplate(template.New("TenantGlobals"), &p.parseContent, tenantGlobals) if err != nil { @@ -278,7 +337,7 @@ func (p *ProjectCommands) generateProjectFiles(gitSpec *git_handler.GitSpec) err return err } - if err := p.writeProjectFiles(filepath.Join(env.secretsPath, system.SecretSpecFile), tSecretSpec); err != nil { + if err := p.writeProjectFiles(filepath.Join(env.secretsPath, util.SecretSpecFile), tSecretSpec); err != nil { return err } @@ -294,24 +353,22 @@ func (p *ProjectCommands) generateProjectFiles(gitSpec *git_handler.GitSpec) err return err } - if err := p.writeProjectFiles(filepath.Join(env.secretsPath, system.SecretSpecFile), secretSpecFile); err != nil { + if err := p.writeProjectFiles(filepath.Join(env.secretsPath, util.SecretSpecFile), secretSpecFile); err != nil { return err } } - if sc.name != "clusters" { - if err := p.writeProjectFiles(filepath.Join(env.secretsPath, system.SopsConfigFile), sopsConfigFile); err != nil { - return err - } + if err := p.writeProjectFiles(filepath.Join(env.secretsPath, util.SopsConfigFile), sopsConfigFile); err != nil { + return err } } } - if err := p.writeProjectFiles(system.GetPwdPath(system.TenantProjectGitIgn), gitignore); err != nil { + if err := p.writeProjectFiles(util.GetPwdPath(util.TenantProjectGitIgn), gitignore); err != nil { return err } - if err := p.writeProjectFiles(system.GetPwdPath(system.TenantProjectCodeOwners), p.owners); err != nil { + if err := p.writeProjectFiles(util.GetPwdPath(util.TenantProjectCodeOwners), p.owners); err != nil { return err } @@ -342,36 +399,27 @@ func (p *ProjectCommands) generateProject(gitSpec *git_handler.GitSpec) error { } if reflect.ValueOf(p.projectFile.Spec).IsZero() { - return fmt.Errorf("'spec' option required in %s", system.TenantProjectFile) + return fmt.Errorf("'spec' option required in %s", util.TenantProjectFile) } switch { case len(p.projectFile.Spec.Scopes) == 0 && len(p.projectFile.Spec.Environments) > 0: - return fmt.Errorf("'scopes' option required, if 'environments' specified in %s", system.TenantProjectFile) + return fmt.Errorf("'scopes' option required, if 'environments' specified in %s", util.TenantProjectFile) case len(p.projectFile.Spec.Scopes) > 0 && len(p.projectFile.Spec.Environments) == 0: - return fmt.Errorf("'environments' option required, if 'scopes' specified in %s", system.TenantProjectFile) + return fmt.Errorf("'environments' option required, if 'scopes' specified in %s", util.TenantProjectFile) case len(p.projectFile.Spec.Scopes) == 0 && len(p.projectFile.Spec.Environments) == 0: - return fmt.Errorf("'scopes', 'environments' options required in %s", system.TenantProjectFile) + return fmt.Errorf("'scopes', 'environments' options required in %s", util.TenantProjectFile) } for sKey, sc := range p.projectFile.Spec.Scopes { p.Scopes = append(p.Scopes, sc) p.scopes = append(p.scopes, scope{name: sc, environments: make(map[string]*environment)}) - for _, env := range p.projectFile.Spec.Environments { - if sc == "clusters" { - p.scopes[sKey].environments[env] = &environment{ - secretsPath: system.GetPwdPath(system.TenantValuesDIR, sc, p.Conf.ClusterProvider, env, "secrets"), - valuesPath: system.GetPwdPath(system.TenantValuesDIR, sc, p.Conf.ClusterProvider, env, "values"), - } - - continue - } - + for env := range p.projectFile.Spec.Environments { p.scopes[sKey].environments[env] = &environment{ - globalsPath: system.GetPwdPath(system.TenantValuesDIR, sc, env, system.GlobalsFileName), - releasesPath: system.GetPwdPath(system.TenantValuesDIR, sc, env, system.ReleasesFileName), - secretsPath: system.GetPwdPath(system.TenantValuesDIR, sc, env, "secrets"), - valuesPath: system.GetPwdPath(system.TenantValuesDIR, sc, env, "values"), + globalsPath: util.GetPwdPath(util.TenantValuesDIR, sc, env, util.GlobalsFileName), + releasesPath: util.GetPwdPath(util.TenantValuesDIR, sc, env, util.ReleasesFileName), + secretsPath: util.GetPwdPath(util.TenantValuesDIR, sc, env, "secrets"), + valuesPath: util.GetPwdPath(util.TenantValuesDIR, sc, env, "values"), } } } @@ -388,7 +436,7 @@ func (p *ProjectCommands) generateProject(gitSpec *git_handler.GitSpec) error { } } - if err := os.MkdirAll(system.GetPwdPath("docs"), 0755); err != nil { + if err := os.MkdirAll(util.GetPwdPath("docs"), 0755); err != nil { return err } @@ -397,7 +445,7 @@ func (p *ProjectCommands) generateProject(gitSpec *git_handler.GitSpec) error { } if p.Ctx.Bool("create-sops-age-keys") { - if err := newSecretCommands(p.Conf, p.Ctx, system.GetPwdPath()).CreateKeys(); err != nil { + if err := newSecretCommands(p.Conf, p.Ctx, util.GetPwdPath()).CreateKeys(); err != nil { return err } } @@ -407,32 +455,20 @@ func (p *ProjectCommands) generateProject(gitSpec *git_handler.GitSpec) error { func projectGenerateAction(conf *config.Config, gitSpec *git_handler.GitSpec) cli.ActionFunc { return func(c *cli.Context) error { - if err := system.ValidateArtifactModeDefault(c, ""); err != nil { - return err - } - - if err := system.ValidateNArg(c, 0); err != nil { + if err := util.ValidateNArg(c, 0); err != nil { return err } - if err := newProjectCommand(conf, c, system.GetPwdPath()).generateProject(gitSpec); err != nil { - return err - } - - return resolveDependencies(conf.InitConfig(false), c, false) + return newProjectCommand(conf, c, util.GetPwdPath(), gitSpec).generateProject(gitSpec) } } func projectUpdateAction(conf *config.Config, gitSpec *git_handler.GitSpec) cli.ActionFunc { return func(c *cli.Context) error { - if err := system.ValidateArtifactModeDefault(c, ""); err != nil { - return err - } - - if err := system.ValidateNArg(c, 0); err != nil { + if err := util.ValidateNArg(c, 0); err != nil { return err } - return newProjectCommand(conf, c, system.GetPwdPath()).updateProjectFile(gitSpec) + return newProjectCommand(conf, c, util.GetPwdPath(), gitSpec).updateProjectFile(gitSpec) } } diff --git a/commands/download_packages.go b/cmd/project_dependencies.go similarity index 50% rename from commands/download_packages.go rename to cmd/project_dependencies.go index 3c253a4..39b0b1c 100644 --- a/commands/download_packages.go +++ b/cmd/project_dependencies.go @@ -1,25 +1,19 @@ -package commands +package cmd import ( - "encoding/json" "fmt" "net/http" "net/url" "os" - "path" "path/filepath" "strings" - "syscall" - "time" "github.com/Masterminds/semver" "github.com/urfave/cli/v2" "go.uber.org/zap" - "rmk/aws_provider" "rmk/config" - "rmk/go_getter" - "rmk/system" + "rmk/util" ) const ( @@ -28,9 +22,8 @@ const ( ) var ( - TenantPrDependenciesDir = filepath.Join(system.TenantProjectDIR, "dependencies") - TenantPrInventoryDir = filepath.Join(system.TenantProjectDIR, "inventory") - TenantPrInvClustersDir = filepath.Join(TenantPrInventoryDir, "clusters") + TenantPrDependenciesDir = filepath.Join(util.TenantProjectDIR, "dependencies") + TenantPrInventoryDir = filepath.Join(util.TenantProjectDIR, "inventory") TenantPrInvHooksDir = filepath.Join(TenantPrInventoryDir, "hooks") ) @@ -43,31 +36,9 @@ type SpecDownload struct { PkgName string Header *http.Header Type string - Artifact *ArtifactSpec rmOldDir bool } -type ArtifactSpec struct { - BucketName string - Key string - Region string - Version string - Url string -} - -type RMKArtifactMetadata struct { - ProjectName string `json:"project_name"` - Tag string `json:"tag"` - PreviousTag string `json:"previous_tag"` - Version string `json:"version"` - Commit string `json:"commit"` - Date time.Time `json:"date"` - Runtime struct { - Goos string `json:"goos"` - Goarch string `json:"goarch"` - } `json:"runtime"` -} - type InventoryState struct { clustersState map[string]struct{} helmPluginsState map[string]struct{} @@ -124,150 +95,17 @@ func (s *SpecDownload) download(silent bool) error { } return s.downloadErrorHandler( - go_getter.DownloadArtifact(s.PkgUrl, s.PkgDst, s.PkgName, s.Header, silent, s.Conf.ProgressBar, s.Ctx.Context), + util.DownloadArtifact(s.PkgUrl, s.PkgDst, s.PkgName, s.Header, silent, s.Conf.ProgressBar, s.Ctx.Context), ) } -func (s *SpecDownload) parseArtifactUrl() error { - u, err := url.Parse(s.Artifact.Url) - if err != nil { - return err - } - - // This just check whether we are dealing with S3 or - // any other S3 compliant service. S3 has a predictable - // url as others do not - if strings.Contains(u.Host, "amazonaws.com") { - // Amazon S3 supports both virtual-hosted–style and path-style URLs to access a bucket, although path-style is deprecated - // In both cases few older regions supports dash-style region indication (s3-Region) even if AWS discourages their use. - // The same bucket could be reached with: - // bucket.s3.region.amazonaws.com/path - // bucket.s3-region.amazonaws.com/path - // s3.amazonaws.com/bucket/path - // s3-region.amazonaws.com/bucket/path - - hostParts := strings.Split(u.Host, ".") - switch len(hostParts) { - // path-style - case 3: - // Parse the region out of the first part of the host - s.Artifact.Region = strings.TrimPrefix(strings.TrimPrefix(hostParts[0], "s3-"), "s3") - if s.Artifact.Region == "" { - s.Artifact.Region = "us-east-1" - } - - pathParts := strings.SplitN(u.Path, "/", 3) - s.Artifact.BucketName = pathParts[1] - s.Artifact.Key = pathParts[2] - // vhost-style, dash region indication - case 4: - // Parse the region out of the first part of the host - s.Artifact.Region = strings.TrimPrefix(strings.TrimPrefix(hostParts[1], "s3-"), "s3") - if s.Artifact.Region == "" { - return fmt.Errorf("artifact URL is not valid S3 URL for dependency name: %s", s.PkgName) - } - - pathParts := strings.SplitN(u.Path, "/", 2) - s.Artifact.BucketName = hostParts[0] - s.Artifact.Key = pathParts[1] - //vhost-style, dot region indication - case 5: - s.Artifact.Region = hostParts[2] - pathParts := strings.SplitN(u.Path, "/", 2) - s.Artifact.BucketName = hostParts[0] - s.Artifact.Key = pathParts[1] - - } - - if len(hostParts) < 3 && len(hostParts) > 5 { - return fmt.Errorf("artifact URL is not valid S3 URL for dependency name: %s", s.PkgName) - } - - if len(strings.SplitN(s.Artifact.Key, "/", 2)) < 1 { - return fmt.Errorf("artifact URL is not valid S3 URL for dependency name %s, path does not contain SemVer2", s.PkgName) - } - - s.Artifact.Version = strings.SplitN(s.Artifact.Key, "/", 2)[0] - - _, err := semver.NewVersion(s.Artifact.Version) - if err != nil { - return fmt.Errorf("%s for dependency name: %s", err.Error(), s.PkgName) - } - - return nil - } else { - return fmt.Errorf("artifact URL is not valid S3 compliant URL for dependency name: %s", s.PkgName) - } -} - -func (s *SpecDownload) updateArtifact() error { - currentProfile := s.Conf.Profile - licenseProfile := s.Conf.Profile + "-license" - - s.Conf.AwsConfigure.Profile = licenseProfile - if ok, err := s.Conf.GetAwsConfigure(licenseProfile); err != nil && ok { - zap.S().Warnf("%s", err.Error()) - if err := newConfigCommands(s.Conf, s.Ctx, system.GetPwdPath("")).configAws(); err != nil { - return err - } - - if _, err := s.Conf.GetAwsConfigure(licenseProfile); err != nil { - return err - } - } else if s.Ctx.Bool("aws-reconfigure-artifact-license") { - if err := newConfigCommands(s.Conf, s.Ctx, system.GetPwdPath("")).configAws(); err != nil { - return err - } - } - - if err := s.parseArtifactUrl(); err != nil { - return err - } - - if artVerExist, err := s.Conf.BucketKeyExists(s.Artifact.Region, s.Artifact.BucketName, s.Artifact.Key); err != nil || !artVerExist { - return err - } - - if err := s.Conf.DownloadFromBucket(s.Artifact.Region, s.Artifact.BucketName, system.GetPwdPath(system.ArtifactDownloadDir), s.Artifact.Key); err != nil { - return err - } - - s.Conf.AwsConfigure.Profile = currentProfile - if system.IsExists(s.PkgDst, false) { - if err := os.MkdirAll(s.PkgDst, 0777); err != nil { - return err - } - } - - r, err := os.Open(filepath.Join(system.GetPwdPath(system.ArtifactDownloadDir), s.Artifact.Key)) - if err != nil { - return err - } - - if err := system.UnTar(s.PkgDst, "", r); err != nil { - return err - } - - if system.IsExists(filepath.Join(s.PkgDst, system.TenantProjectDIR, "inventory"), false) { - if err := system.CopyDir(filepath.Join(s.PkgDst, system.TenantProjectDIR, "inventory"), system.GetPwdPath(system.TenantProjectDIR)); err != nil { - return err - } - } - - if err := os.RemoveAll(system.GetPwdPath(system.ArtifactDownloadDir)); err != nil { - return err - } - - return nil -} - // depsGC - deleting old deps dirs with not actual versions func hooksGC(hooks []config.HookMapping) error { - if !system.IsExists(system.GetPwdPath(TenantPrInvHooksDir), false) { + if !util.IsExists(util.GetPwdPath(TenantPrInvHooksDir), false) { return nil } - allDirs, _, err := system.ListDir(system.GetPwdPath(TenantPrInvHooksDir), true) + allDirs, _, err := util.ListDir(util.GetPwdPath(TenantPrInvHooksDir), true) if err != nil { return err } @@ -367,11 +205,6 @@ func uniqueHooksMapping(hooks []config.HookMapping) []config.HookMapping { } func (is *InventoryState) saveState(inv config.Inventory) { - is.clustersState = make(map[string]struct{}) - for key := range inv.Clusters { - is.clustersState[key] = struct{}{} - } - is.helmPluginsState = make(map[string]struct{}) for key := range inv.HelmPlugins { is.helmPluginsState[key] = struct{}{} @@ -383,26 +216,6 @@ func (is *InventoryState) saveState(inv config.Inventory) { } } -func (is *InventoryState) resolveClusters(invPkg map[string]*config.Package, conf *config.Config) (map[string]*config.Package, error) { - if len(conf.Clusters) == 0 { - conf.Clusters = make(map[string]*config.Package) - } - - for key, pkg := range invPkg { - vPkg, _ := semver.NewVersion(pkg.Version) - if _, ok := conf.Clusters[key]; !ok { - conf.Clusters[key] = pkg - } else if _, found := is.clustersState[key]; !found { - vP, _ := semver.NewVersion(conf.Clusters[key].Version) - if vPkg.GreaterThan(vP) { - conf.Clusters[key] = pkg - } - } - } - - return conf.Clusters, nil -} - func (is *InventoryState) resolveHelmPlugins(invPkg map[string]*config.Package, conf *config.Config) (map[string]*config.Package, error) { if len(conf.HelmPlugins) == 0 { conf.HelmPlugins = make(map[string]*config.Package) @@ -464,16 +277,11 @@ func resolveDependencies(conf *config.Config, ctx *cli.Context, silent bool) err for _, val := range conf.Dependencies { projectFile := &config.ProjectFile{} - depsDir := system.FindDir(system.GetPwdPath(TenantPrDependenciesDir), val.Name) - if err := projectFile.ReadProjectFile(system.GetPwdPath(TenantPrDependenciesDir, depsDir, system.TenantProjectFile)); err != nil { + depsDir := util.FindDir(util.GetPwdPath(TenantPrDependenciesDir), val.Name) + if err := projectFile.ReadProjectFile(util.GetPwdPath(TenantPrDependenciesDir, depsDir, util.TenantProjectFile)); err != nil { return err } - // Resolve and recursively download repositories containing clusters - if conf.Clusters, invErr = invState.resolveClusters(projectFile.Clusters, conf); invErr != nil { - return invErr - } - // Resolve and recursively download repositories containing helm plugins if conf.HelmPlugins, invErr = invState.resolveHelmPlugins(projectFile.HelmPlugins, conf); invErr != nil { return invErr @@ -527,10 +335,6 @@ func resolveDependencies(conf *config.Config, ctx *cli.Context, silent bool) err return err } - if err := updateClusters(conf, ctx, silent); err != nil { - return err - } - // Finding unique versions of hooks in HooksMapping conf.HooksMapping = uniqueHooksMapping(conf.HooksMapping) @@ -547,7 +351,7 @@ func resolveDependencies(conf *config.Config, ctx *cli.Context, silent bool) err return err } - if err := newConfigCommands(conf, ctx, system.GetPwdPath("")).configHelmPlugins(); err != nil { + if err := newConfigCommands(conf, ctx, util.GetPwdPath("")).configHelmPlugins(); err != nil { return err } @@ -559,11 +363,11 @@ func resolveDependencies(conf *config.Config, ctx *cli.Context, silent bool) err } func removeOldDir(pwd string, pkg config.Package) error { - if !system.IsExists(pwd, false) { + if !util.IsExists(pwd, false) { return nil } - oldDir := system.FindDir(pwd, pkg.Name) + oldDir := util.FindDir(pwd, pkg.Name) if len(strings.Split(oldDir, "-")) > 1 { oldVer := strings.SplitN(oldDir, "-", 2)[1] if oldVer != pkg.Version { @@ -580,28 +384,16 @@ func (s *SpecDownload) batchUpdate(pwd string, pkg config.Package, silent bool) s.PkgUrl = pkg.Url s.PkgName = pkg.Name + "-" + strings.ReplaceAll(pkg.Version, "/", "_") s.PkgDst = filepath.Join(s.PkgDst, s.PkgName) - pkgExists := system.IsExists(s.PkgDst, false) + pkgExists := util.IsExists(s.PkgDst, false) if !pkgExists { - if s.rmOldDir && s.Ctx.String("artifact-mode") != system.ArtifactModeOnline { + if s.rmOldDir { if err := removeOldDir(pwd, pkg); err != nil { return err } } - switch { - case s.Ctx.String("artifact-mode") == system.ArtifactModeOnline && len(pkg.ArtifactUrl) > 0: - if err := s.updateArtifact(); err != nil { - return err - } - case s.Ctx.String("artifact-mode") == system.ArtifactModeOnline && len(pkg.ArtifactUrl) == 0: - zap.S().Warnf("overriding %s component in inventory section "+ - "%s file is not allowed when using %s artifact mode", - s.PkgName, system.TenantProjectFile, system.ArtifactModeOnline) - return nil - default: - if err := s.download(silent); err != nil { - return err - } + if err := s.download(silent); err != nil { + return err } } @@ -609,11 +401,10 @@ func (s *SpecDownload) batchUpdate(pwd string, pkg config.Package, silent bool) } func updateDependencies(conf *config.Config, ctx *cli.Context, silent bool) error { - pwd := system.GetPwdPath(TenantPrDependenciesDir) + pwd := util.GetPwdPath(TenantPrDependenciesDir) for key, val := range conf.Dependencies { - spec := &SpecDownload{Conf: conf, Ctx: ctx, PkgDst: pwd, - Artifact: &ArtifactSpec{Url: val.ArtifactUrl}, rmOldDir: true} + spec := &SpecDownload{Conf: conf, Ctx: ctx, PkgDst: pwd, rmOldDir: true} if err := spec.batchUpdate(pwd, val, silent); err != nil { return err } @@ -625,39 +416,24 @@ func updateDependencies(conf *config.Config, ctx *cli.Context, silent bool) erro } switch { - case system.IsExists(filepath.Join(spec.PkgDst, system.HelmfileFileName), true): - conf.Dependencies[key].DstPath = filepath.Join(spec.PkgDst, system.HelmfileFileName) - case system.IsExists(filepath.Join(spec.PkgDst, system.HelmfileGoTmplName), true): - conf.Dependencies[key].DstPath = filepath.Join(spec.PkgDst, system.HelmfileGoTmplName) + case util.IsExists(filepath.Join(spec.PkgDst, util.HelmfileFileName), true): + conf.Dependencies[key].DstPath = filepath.Join(spec.PkgDst, util.HelmfileFileName) + case util.IsExists(filepath.Join(spec.PkgDst, util.HelmfileGoTmplName), true): + conf.Dependencies[key].DstPath = filepath.Join(spec.PkgDst, util.HelmfileGoTmplName) default: return fmt.Errorf("%s or %s not found in dependent project %s", - system.HelmfileFileName, system.HelmfileGoTmplName, spec.PkgName) + util.HelmfileFileName, util.HelmfileGoTmplName, spec.PkgName) } } return nil } -func updateClusters(conf *config.Config, ctx *cli.Context, silent bool) error { - pwd := system.GetPwdPath(TenantPrInvClustersDir) - - for key, val := range conf.Clusters { - spec := &SpecDownload{Conf: conf, Ctx: ctx, PkgDst: pwd, Artifact: &ArtifactSpec{}, rmOldDir: true} - if err := spec.batchUpdate(pwd, *val, silent); err != nil { - return err - } - - conf.Clusters[key].DstPath = spec.PkgDst - } - - return nil -} - func updateHooks(conf *config.Config, ctx *cli.Context, silent bool) error { - pwd := system.GetPwdPath(TenantPrInvHooksDir) + pwd := util.GetPwdPath(TenantPrInvHooksDir) for key, val := range conf.HooksMapping { - spec := &SpecDownload{Conf: conf, Ctx: ctx, PkgDst: pwd, Artifact: &ArtifactSpec{}} + spec := &SpecDownload{Conf: conf, Ctx: ctx, PkgDst: pwd} if err := spec.batchUpdate(pwd, *val.Package, silent); err != nil { return err } @@ -675,7 +451,7 @@ func match(dir string, patterns []string) ([]string, error) { ) for _, val := range patterns { - match, err := system.WalkMatch(dir, val) + match, err := util.WalkMatch(dir, val) if err != nil { return nil, err } @@ -703,7 +479,7 @@ func match(dir string, patterns []string) ([]string, error) { func overwriteFiles(path, pattern, name string) error { var data []byte - oldFilePath, err := system.WalkMatch(path, pattern) + oldFilePath, err := util.WalkMatch(path, pattern) if err != nil { return err } @@ -719,13 +495,13 @@ func updateTools(conf *config.Config, ctx *cli.Context, silent bool) error { spec := &SpecDownload{ Conf: conf, Ctx: ctx, - PkgDst: system.GetHomePath(system.RMKDir, system.RMKToolsDir, system.ToolsTmpDir), + PkgDst: util.GetHomePath(util.RMKDir, util.RMKToolsDir, util.ToolsTmpDir), Type: httpProvider, } - toolsVersionPath := system.GetHomePath(system.RMKDir, system.RMKToolsDir, system.ToolsVersionDir) - toolsTmpPath := system.GetHomePath(system.RMKDir, system.RMKToolsDir, system.ToolsTmpDir) - toolsBinPath := system.GetHomePath(".local", system.ToolsBinDir) + toolsVersionPath := util.GetHomePath(util.RMKDir, util.RMKToolsDir, util.ToolsVersionDir) + toolsTmpPath := util.GetHomePath(util.RMKDir, util.RMKToolsDir, util.ToolsTmpDir) + toolsBinPath := util.GetHomePath(".local", util.ToolsBinDir) if err := os.MkdirAll(toolsVersionPath, 0755); err != nil { return err @@ -740,7 +516,7 @@ func updateTools(conf *config.Config, ctx *cli.Context, silent bool) error { version, _ := semver.NewVersion(val.Version) spec.PkgUrl = val.Url spec.PkgName = val.Name + "-" + version.String() - if !system.IsExists(filepath.Join(toolsVersionPath, spec.PkgName), true) { + if !util.IsExists(filepath.Join(toolsVersionPath, spec.PkgName), true) { err := spec.download(silent) if err != nil { return err @@ -775,11 +551,11 @@ func updateTools(conf *config.Config, ctx *cli.Context, silent bool) error { for _, pkg := range conf.Tools { for _, pathArt := range pkg.Artifacts { if pkg.Rename { - if err := system.CopyFile(pathArt, filepath.Join(toolsBinPath, pkg.Name)); err != nil { + if err := util.CopyFile(pathArt, filepath.Join(toolsBinPath, pkg.Name)); err != nil { return err } } else { - if err := system.CopyFile(pathArt, filepath.Join(toolsBinPath, filepath.Base(pathArt))); err != nil { + if err := util.CopyFile(pathArt, filepath.Join(toolsBinPath, filepath.Base(pathArt))); err != nil { return err } } @@ -788,66 +564,3 @@ func updateTools(conf *config.Config, ctx *cli.Context, silent bool) error { return os.RemoveAll(toolsTmpPath) } - -func getRMKArtifactMetadata(keyPath string) (*RMKArtifactMetadata, error) { - rmkArtifactMetadata := &RMKArtifactMetadata{} - aws := &aws_provider.AwsConfigure{Region: system.RMKBucketRegion} - data, err := aws.GetFileData(system.RMKBucketName, system.RMKBin+"/"+keyPath+"/metadata.json") - if err != nil { - return nil, err - } - - if err := json.Unmarshal(data, &rmkArtifactMetadata); err != nil { - return nil, err - } - - return rmkArtifactMetadata, nil -} - -func rmkURLFormation(paths ...string) string { - u, err := url.Parse("https://" + system.RMKBucketName + ".s3." + system.RMKBucketRegion + ".amazonaws.com") - if err != nil { - zap.S().Fatal(err) - } - - p := append([]string{u.Path}, paths...) - u.Path = path.Join(p...) - return u.String() -} - -func updateRMK(pkgName, version string, silent, progressBar bool, ctx *cli.Context) error { - zap.S().Infof("starting package download: %s", pkgName) - pkgDst := system.GetHomePath(filepath.Join(".local", system.ToolsBinDir)) - if err := go_getter.DownloadArtifact( - rmkURLFormation(system.RMKBin, version, pkgName), - pkgDst, - pkgName, - &http.Header{}, - silent, - progressBar, - ctx.Context, - ); err != nil { - return err - } - - if err := os.Rename(filepath.Join(pkgDst, pkgName), filepath.Join(pkgDst, system.RMKBin)); err != nil { - return err - } - - if err := os.Chmod(filepath.Join(pkgDst, system.RMKBin), 0755); err != nil { - return err - } - - relPath := strings.ReplaceAll(system.RMKSymLinkPath, filepath.Base(system.RMKSymLinkPath), "") - if syscall.Access(relPath, uint32(2)) == nil { - if !system.IsExists(system.RMKSymLinkPath, true) { - return os.Symlink(filepath.Join(pkgDst, system.RMKBin), system.RMKSymLinkPath) - } - } else { - zap.S().Warnf("symlink was not created automatically due to permissions, "+ - "please complete installation by running command: \n"+ - "sudo ln -s %s %s", filepath.Join(pkgDst, system.RMKBin), system.RMKSymLinkPath) - } - - return nil -} diff --git a/commands/project_generation_category.go b/cmd/project_generation.go similarity index 89% rename from commands/project_generation_category.go rename to cmd/project_generation.go index cfb8a96..3775438 100644 --- a/commands/project_generation_category.go +++ b/cmd/project_generation.go @@ -1,50 +1,6 @@ -package commands +package cmd const ( - clusterVariables = `# Kubernetes user list -k8s_master_usernames = [] -k8s_cluster_version = "" # Actual EKS Kubernetes version -# Bastion -bastion_enabled = false -# IAM Roles -aws_lb_controller_role_enabled = true -external_dns_role_enabled = true -ebs_csi_controller_roles_enabled = true -` - - clusterWorkerGroups = `# For example: -# worker_groups = [ -# { -# name = "" -# instance_type = "t3.medium" -# additional_userdata = "t3.medium" -# asg_desired_capacity = 3 -# asg_max_size = 3 -# asg_min_size = 3 -# ami_id = "ami-" -# kubelet_extra_args = "--node-labels=key1=value1" -# root_volume_size = "10" -# root_volume_type = "gp3" -# enable_monitoring = false -# }, -# { -# name = "" -# instance_type = "t3.medium" -# additional_userdata = "t3.medium" -# asg_desired_capacity = 3 -# asg_max_size = 3 -# asg_min_size = 3 -# ami_id = "ami-" -# kubelet_extra_args = "--node-labels=key2=value2 --register-with-taints=key2=value2:NoSchedule" -# root_volume_size = "10" -# root_volume_type = "gp3" -# enable_monitoring = false -# }, -# ] - -worker_groups = [] -` - codeOwners = `# These owners will be the default owners for everything in # the repo and will be requested for review when someone opens a pull request. ` @@ -146,9 +102,23 @@ helmfiles: ` + escapeOpen + `{{ env "HELMFILE_` + escapeClose + `{{ .TenantNameE ` helmfileReleases = `releases: - # TODO: It is recommended to adapt this example considering security, performance and configuration management - # TODO: requirements specific to your application or infrastructure. + # TODO: Releases from group 1 are needed to deploy K3D clusters. + # TODO: If you do not inherit upstream repositories, you can leave these releases as is, + # TODO: or make sure that upstream repositories do not have the same releases to avoid conflicts. # Group 1 + - name: k3d-cluster + namespace: kube-system + chart: "{{"{{` + escape + `{{ .Release.Labels.repo }}` + escape + `}}"}}/k3d-cluster" + version: 0.1.0 + labels: + cluster: k3d + installed: ` + escapeOpen + `{{ eq (env "K3D_CLUSTER" | default "false") "true" }}` + escapeClose + ` + inherit: + - template: release + + # TODO: It is recommended to adapt this example considering security, performance and configuration management + # TODO: requirements specific to your application or infrastructure. + # Group 2 - name: {{ .TenantName }}-app namespace: {{ .TenantName }} chart: "{{"{{` + escape + `{{ .Release.Labels.repo }}` + escape + `}}"}}/app" @@ -179,15 +149,10 @@ Detailed information about requirements and installation instructions can be fou ### Requirements -- AWS CLI >= 2.9 -- AWS IAM user security credentials (access key pair) - Git -- GitHub PAT to access the following repositories to list in the sections ` + "`project.yaml`" + `: - * clusters - * hooks +- GitHub PAT to access the repositories listed in the ` + "`dependencies`" + ` section of ` + "`project.yaml`" + ` - Note: K3D v5.x.x requires at least Docker v20.10.5 (runc >= v1.0.0-rc93) to work properly -- Python >= 3.9 -- [RMK CLI](https://github.com/edenlabllc/rmk?tab=readme-ov-file#rmk-cli---reduced-management-for-kubernetes) >= v.0.42.4 +- [RMK CLI](https://edenlabllc.github.io/rmk/latest) ### GitLab flow strategy @@ -230,17 +195,25 @@ This example shows how the following options are configured and interact with ea {{- end }} ### Basic RMK commands for project management +#### Project generate + +` + "```" + `shell +rmk project generate \ + --environment=develop.root-domain=localhost \ + --owners=user \ + --scopes={{ .TenantName }} +` + "```" + ` + #### Initialization configuration ` + "```" + `shell rmk config init ` + "```" + ` -#### Cluster provision +#### Create K3D cluster ` + "```" + `shell -rmk cluster provision --plan -rmk cluster provision +rmk cluster k3d create ` + "```" + ` #### Release sync @@ -249,7 +222,7 @@ rmk cluster provision rmk release sync ` + "```" + ` -> Note: A complete list of RMK commands and capabilities can be found at the [link](https://github.com/edenlabllc/rmk?tab=readme-ov-file#rmk-cli---reduced-management-for-kubernetes) +> Note: A complete list of RMK commands and capabilities can be found at the [link](https://edenlabllc.github.io/rmk/latest) ` releasesFile = `# This file defines the release list, is located in the environment directory diff --git a/commands/release_category.go b/cmd/release.go similarity index 64% rename from commands/release_category.go rename to cmd/release.go index 79e3c08..fb4a706 100644 --- a/commands/release_category.go +++ b/cmd/release.go @@ -1,4 +1,4 @@ -package commands +package cmd import ( "bytes" @@ -20,21 +20,25 @@ import ( "rmk/config" "rmk/git_handler" "rmk/notification" - "rmk/system" + "rmk/providers/aws_provider" + "rmk/providers/azure_provider" + "rmk/providers/google_provider" + "rmk/util" ) -type runner interface { +type releaseRunner interface { runCMD() error } type ReleaseCommands struct { Conf *config.Config Ctx *cli.Context - SpecCMD *system.SpecCMD + SpecCMD *util.SpecCMD Scope string WorkDir string ValuesPath string UpdateContext bool + APICluster bool K3DCluster bool } @@ -85,69 +89,34 @@ type HelmStatus struct { Namespace string `json:"namespace"` } -type KubeConfig struct { - Kind string `json:"kind"` - ApiVersion string `json:"apiVersion"` - Preferences struct { - } `json:"preferences"` - Clusters []struct { - Name string `json:"name"` - Cluster struct { - Server string `json:"server"` - CertificateAuthorityData string `json:"certificate-authority-data"` - } `json:"cluster"` - } `json:"clusters"` - Users []struct { - Name string `json:"name"` - User struct { - Exec struct { - Command string `json:"command"` - Args []string `json:"args"` - Env []struct { - Name string `json:"name"` - Value string `json:"value"` - } `json:"env"` - ApiVersion string `json:"apiVersion"` - ProvideClusterInfo bool `json:"provideClusterInfo"` - } `json:"exec"` - } `json:"user"` - } `json:"users"` - Contexts []struct { - Name string `json:"name"` - Context struct { - Cluster string `json:"cluster"` - User string `json:"user"` - } `json:"context"` - } `json:"contexts"` - CurrentContext string `json:"current-context"` -} - func (rc *ReleaseCommands) runCMD() error { - if err := rc.SpecCMD.AddEnv(); err != nil { + if err := rc.SpecCMD.AddOSEnv(); err != nil { return err } if err := rc.SpecCMD.ExecCMD(); err != nil { - if rc.SpecCMD.Debug { - zap.S().Debugf("command: %s", rc.SpecCMD.CommandStr) - zap.S().Debugf("path: %s", rc.SpecCMD.Dir) - for _, val := range rc.SpecCMD.Envs { - zap.S().Debugf("env: %s", strings.ReplaceAll(val, rc.Conf.GitHubToken, "[rmk_sensitive]")) - } - } + rc.debugLevel() return err } + rc.debugLevel() + + return nil +} + +func (rc *ReleaseCommands) debugLevel() { if rc.SpecCMD.Debug { zap.S().Debugf("command: %s", rc.SpecCMD.CommandStr) zap.S().Debugf("path: %s", rc.SpecCMD.Dir) for _, val := range rc.SpecCMD.Envs { - zap.S().Debugf("env: %s", strings.ReplaceAll(val, rc.Conf.GitHubToken, "[rmk_sensitive]")) + if len(rc.Conf.GitHubToken) > 0 { + zap.S().Debugf("env: %s", strings.ReplaceAll(val, rc.Conf.GitHubToken, "[rmk_sensitive]")) + } else { + zap.S().Debugf("env: %s", val) + } } } - - return nil } func (rc *ReleaseCommands) nestedHelmfiles(envs ...string) []string { @@ -164,25 +133,47 @@ func (rc *ReleaseCommands) nestedHelmfiles(envs ...string) []string { return append(envs, hfVersion...) } -func (rc *ReleaseCommands) prepareHelmfile(args ...string) *system.SpecCMD { +func (rc *ReleaseCommands) prepareHelmfile(args ...string) *util.SpecCMD { + var sensKeyWords []string + + defaultArgs := []string{"--environment", rc.Conf.Environment} + + // generating common environment variables envs := append([]string{}, "NAME="+rc.Conf.Name, + "ROOT_DOMAIN="+rc.Conf.RootDomain, + "SOPS_AGE_KEY_FILE="+filepath.Join(rc.Conf.SopsAgeKeys, util.SopsAgeKeyFile), "TENANT="+rc.Conf.Tenant, - "SOPS_AGE_KEY_FILE="+filepath.Join(rc.Conf.SopsAgeKeys, system.SopsAgeKeyFile), - "GITHUB_TOKEN="+rc.Conf.GitHubToken, - "AWS_PROFILE="+rc.Conf.Profile, - "AWS_CONFIG_FILE="+strings.Join(rc.Conf.AWSSharedConfigFile(rc.Conf.Profile), ""), - "AWS_SHARED_CREDENTIALS_FILE="+strings.Join(rc.Conf.AWSSharedCredentialsFile(rc.Conf.Profile), ""), - // Needed to set the AWS region to force the connection session region for the helm S3 plugin, - // if AWS_DEFAULT_REGION and AWS_REGION cannot be trusted. - "HELM_S3_REGION="+rc.Conf.S3ChartsRepoRegion, ) - if _, ok := rc.Conf.Env["ROOT_DOMAIN"]; ok { - envs = append(envs, "ROOT_DOMAIN="+rc.Conf.Env["ROOT_DOMAIN"]) - delete(rc.Conf.Env, "ROOT_DOMAIN") + if len(rc.Conf.GitHubToken) > 0 { + sensKeyWords = []string{rc.Conf.GitHubToken} + envs = append(envs, "GITHUB_TOKEN="+rc.Conf.GitHubToken) } else { - envs = append(envs, "ROOT_DOMAIN="+rc.Conf.RootDomain) + sensKeyWords = []string{} + } + + // generating additional environment variables to specific cluster provider + switch rc.Conf.ClusterProvider { + case aws_provider.AWSClusterProvider: + envs = append(envs, + "AWS_ACCOUNT_ID="+rc.Conf.AccountID, + "AWS_CONFIG_FILE="+strings.Join(rc.Conf.AWSSharedConfigFile(rc.Conf.Profile), ""), + "AWS_PROFILE="+rc.Conf.Profile, + "AWS_REGION="+rc.Conf.Region, + "AWS_SHARED_CREDENTIALS_FILE="+strings.Join(rc.Conf.AWSSharedCredentialsFile(rc.Conf.Profile), ""), + ) + case azure_provider.AzureClusterProvider: + envs = append(envs, + "AZURE_LOCATION="+rc.Conf.AzureConfigure.Location, + "AZURE_SUBSCRIPTION_ID="+rc.Conf.AzureConfigure.SubscriptionID, + ) + case google_provider.GoogleClusterProvider: + envs = append(envs, + "GCP_PROJECT_ID="+rc.Conf.GCPConfigure.ProjectID, + "GCP_REGION="+rc.Conf.GCPRegion, + "GOOGLE_APPLICATION_CREDENTIALS="+rc.Conf.GCPConfigure.AppCredentialsPath, + ) } for _, val := range rc.Conf.HooksMapping { @@ -190,26 +181,22 @@ func (rc *ReleaseCommands) prepareHelmfile(args ...string) *system.SpecCMD { envs = append(envs, "HELMFILE_"+strings.ToUpper(keyTenantEnv)+"_HOOKS_DIR="+val.DstPath) } - for key, val := range rc.Conf.Env { - envs = append(envs, key+"="+val) - } - // generating additional environment variables to nested helmfiles envs = rc.nestedHelmfiles(envs...) - if rc.K3DCluster { + switch { + case rc.APICluster: + envs = append(envs, "CAPI_CLUSTER="+strconv.FormatBool(rc.APICluster)) + case rc.K3DCluster: envs = append(envs, "K3D_CLUSTER="+strconv.FormatBool(rc.K3DCluster)) } - // needed if not used artifact mode - var sensKeyWords []string - if rc.Ctx.String("artifact-mode") == system.ArtifactModeDefault { - sensKeyWords = []string{rc.Conf.GitHubToken} + if len(rc.Ctx.String("helmfile-log-level")) > 0 { + defaultArgs = append(defaultArgs, "--log-level", rc.Ctx.String("helmfile-log-level")) } - return &system.SpecCMD{ - Args: append([]string{"--environment", rc.Conf.Environment, "--log-level", - rc.Ctx.String("helmfile-log-level")}, args...), + return &util.SpecCMD{ + Args: append(defaultArgs, args...), Command: "helmfile", Ctx: rc.Ctx.Context, Dir: rc.WorkDir, @@ -219,33 +206,25 @@ func (rc *ReleaseCommands) prepareHelmfile(args ...string) *system.SpecCMD { } } -func (rc *ReleaseCommands) kubeConfig() *system.SpecCMD { - return &system.SpecCMD{ - Args: []string{"config"}, - Command: "kubectl", - Ctx: rc.Ctx.Context, - Dir: rc.WorkDir, - DisableStdOut: true, - Debug: false, - } -} - func (rc *ReleaseCommands) releaseMiddleware() error { - if len(rc.Conf.Dependencies) == 0 && rc.Ctx.String("artifact-mode") == system.ArtifactModeDefault { + if len(rc.Conf.Dependencies) == 0 { if err := os.RemoveAll(filepath.Join(rc.WorkDir, TenantPrDependenciesDir)); err != nil { return err } } - if err := system.MergeAgeKeys(rc.Conf.SopsAgeKeys); err != nil { + if err := util.MergeAgeKeys(rc.Conf.SopsAgeKeys); err != nil { return err } - if _, currentContext, err := rc.getKubeContext(); err != nil { + if _, currentContext, err := clusterRunner(&ClusterCommands{rc}).getKubeContext(); err != nil { return err } else { - if strings.Contains(currentContext, system.K3DConfigPrefix) { + switch { + case strings.Contains(currentContext, util.K3DPrefix) && !strings.Contains(currentContext, util.CAPI): rc.K3DCluster = true + case currentContext == util.K3DPrefix+"-"+util.CAPI: + rc.APICluster = true } } @@ -262,102 +241,8 @@ func (rc *ReleaseCommands) releaseHelmfile(args ...string) error { return rc.runCMD() } -func (rc *ReleaseCommands) getKubeContext() (string, string, error) { - var ( - contextNames []string - contextName string - ) - - kubeConfig := &KubeConfig{} - - rc.SpecCMD = rc.kubeConfig() - rc.SpecCMD.Args = append(rc.SpecCMD.Args, "view", "--output", "json") - if err := rc.runCMD(); err != nil { - return "", "", fmt.Errorf("Kubectl config failed to view\n%s", rc.SpecCMD.StderrBuf.String()) - } - - if err := json.Unmarshal(rc.SpecCMD.StdoutBuf.Bytes(), &kubeConfig); err != nil { - return "", "", err - } - - re, err := regexp.Compile(`(?i)\b` + rc.Conf.Name + `\b`) - if err != nil { - return "", "", err - } - - for _, val := range kubeConfig.Contexts { - if re.MatchString(val.Name) { - contextNames = append(contextNames, val.Name) - } - } - - switch { - case len(contextNames) > 1: - return "", "", - fmt.Errorf("detected more than one Kubernetes context with names %s leading to conflict, "+ - "please delete or rename all contexts except one", strings.Join(contextNames, ", ")) - case len(contextNames) > 0: - contextName = contextNames[0] - default: - contextName = "" - } - - if rc.K3DCluster && len(contextName) > 0 && !strings.Contains(contextName, system.K3DConfigPrefix) { - return "", "", fmt.Errorf("remote Kubernetes context already exists %s for this branch", contextName) - } - - return contextName, kubeConfig.CurrentContext, nil -} - -func (rc *ReleaseCommands) releaseKubeContext() error { - contextName, currentContextName, err := rc.getKubeContext() - if err != nil { - return err - } - - if len(contextName) > 0 && !rc.UpdateContext { - if contextName != currentContextName { - rc.SpecCMD = rc.kubeConfig() - rc.SpecCMD.Args = append(rc.SpecCMD.Args, "use", contextName) - rc.SpecCMD.DisableStdOut = false - return rc.runCMD() - } - - return nil - } - - if strings.Contains(contextName, system.K3DConfigPrefix) && rc.UpdateContext { - return fmt.Errorf("current context %s already used for K3D cluster, --force flag cannot be used", contextName) - } - - cc := &ClusterCommands{ - Conf: rc.Conf, - Ctx: rc.Ctx, - WorkDir: system.GetPwdPath(""), - } - - if err := cc.clusterContext(); err != nil { - return err - } - - _, currentContext, err := rc.getKubeContext() - if err != nil { - return err - } - - rc.SpecCMD = rc.kubeConfig() - rc.SpecCMD.Args = append(rc.SpecCMD.Args, - "set-credentials", currentContext, - "--exec-env", "AWS_CONFIG_FILE="+strings.Join(rc.Conf.AWSSharedConfigFile(cc.Conf.Profile), ""), - "--exec-env", "AWS_SHARED_CREDENTIALS_FILE="+strings.Join(rc.Conf.AWSSharedCredentialsFile(cc.Conf.Profile), ""), - ) - rc.SpecCMD.DisableStdOut = true - rc.SpecCMD.Debug = true - return rc.runCMD() -} - func (sr *SpecRelease) searchReleasesPath() error { - paths, err := system.WalkInDir(system.GetPwdPath(system.TenantValuesDIR), sr.Conf.Environment, system.ReleasesFileName) + paths, err := util.WalkInDir(util.GetPwdPath(util.TenantValuesDIR), sr.Conf.Environment, util.ReleasesFileName) if err != nil { return err } @@ -419,7 +304,7 @@ func (sr *SpecRelease) updateReleasesFile(g *git_handler.GitSpec) error { } if len(sr.ReleasesPaths) == 0 { - return fmt.Errorf("no files %s found", system.ReleasesFileName) + return fmt.Errorf("no files %s found", util.ReleasesFileName) } sr.Changes.List = make(map[string][]string) @@ -519,8 +404,8 @@ func (sr *SpecRelease) deployUpdatedReleases() error { return sr.runCMD() } -func (rc *ReleaseCommands) helmCommands(args ...string) *system.SpecCMD { - return &system.SpecCMD{ +func (rc *ReleaseCommands) helmCommands(args ...string) *util.SpecCMD { + return &util.SpecCMD{ Args: args, Command: "helm", Ctx: rc.Ctx.Context, @@ -650,26 +535,22 @@ func (sr *SpecRelease) checkStatusRelease() error { func releaseHelmfileAction(conf *config.Config) cli.ActionFunc { return func(c *cli.Context) error { - if err := system.ValidateArtifactModeDefault(c, ""); err != nil { + if err := util.ValidateNArg(c, 0); err != nil { return err } - if err := system.ValidateNArg(c, 0); err != nil { - return err - } - - if err := resolveDependencies(conf.InitConfig(false), c, false); err != nil { + if err := resolveDependencies(conf.InitConfig(), c, false); err != nil { return err } rc := &ReleaseCommands{ Conf: conf, Ctx: c, - WorkDir: system.GetPwdPath(""), + WorkDir: util.GetPwdPath(""), } if !c.Bool("skip-context-switch") { - if err := rc.releaseKubeContext(); err != nil { + if err := clusterRunner(&ClusterCommands{rc}).switchKubeContext(); err != nil { return err } } @@ -703,15 +584,11 @@ func releaseHelmfileAction(conf *config.Config) cli.ActionFunc { func releaseRollbackAction(conf *config.Config) cli.ActionFunc { return func(c *cli.Context) error { - if err := system.ValidateArtifactModeDefault(c, ""); err != nil { - return err - } - - if err := system.ValidateNArg(c, 0); err != nil { + if err := util.ValidateNArg(c, 0); err != nil { return err } - if err := resolveDependencies(conf.InitConfig(false), c, false); err != nil { + if err := resolveDependencies(conf.InitConfig(), c, false); err != nil { return err } @@ -721,10 +598,10 @@ func releaseRollbackAction(conf *config.Config) cli.ActionFunc { }{List: make(map[string][]string)}}} sr.Conf = conf sr.Ctx = c - sr.WorkDir = system.GetPwdPath("") + sr.WorkDir = util.GetPwdPath("") if !c.Bool("skip-context-switch") { - if err := sr.releaseKubeContext(); err != nil { + if err := clusterRunner(&ClusterCommands{&sr.ReleaseCommands}).switchKubeContext(); err != nil { return err } } @@ -740,25 +617,21 @@ func releaseRollbackAction(conf *config.Config) cli.ActionFunc { func releaseUpdateAction(conf *config.Config, gitSpec *git_handler.GitSpec) cli.ActionFunc { return func(c *cli.Context) error { - if err := system.ValidateArtifactModeDefault(c, ""); err != nil { - return err - } - - if err := system.ValidateNArg(c, 0); err != nil { + if err := util.ValidateNArg(c, 0); err != nil { return err } - if err := resolveDependencies(conf.InitConfig(false), c, false); err != nil { + if err := resolveDependencies(conf.InitConfig(), c, false); err != nil { return err } sr := &SpecRelease{} sr.Conf = conf sr.Ctx = c - sr.WorkDir = system.GetPwdPath("") + sr.WorkDir = util.GetPwdPath("") if !c.Bool("skip-context-switch") { - if err := sr.releaseKubeContext(); err != nil { + if err := clusterRunner(&ClusterCommands{&sr.ReleaseCommands}).switchKubeContext(); err != nil { return err } } diff --git a/cmd/rmk.go b/cmd/rmk.go new file mode 100644 index 0000000..38e792f --- /dev/null +++ b/cmd/rmk.go @@ -0,0 +1,175 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + "os" + "path" + "path/filepath" + "strings" + "syscall" + "time" + + "github.com/Masterminds/semver" + "github.com/urfave/cli/v2" + "go.uber.org/zap" + + "rmk/providers/aws_provider" + "rmk/util" +) + +type RMKArtifactMetadata struct { + ProjectName string `json:"project_name"` + Tag string `json:"tag"` + PreviousTag string `json:"previous_tag"` + Version string `json:"version"` + Commit string `json:"commit"` + Date time.Time `json:"date"` + Runtime struct { + Goos string `json:"goos"` + Goarch string `json:"goarch"` + } `json:"runtime"` +} + +func completionAction() cli.ActionFunc { + return func(c *cli.Context) error { + if err := util.ValidateNArg(c, 0); err != nil { + return err + } + + fmt.Println(util.CompletionZshScript) + + return nil + } +} + +func docGenerateAction() cli.ActionFunc { + return func(c *cli.Context) error { + if err := util.ValidateNArg(c, 0); err != nil { + return err + } + + man, err := c.App.ToMarkdown() + if err != nil { + return nil + } + + fmt.Println(man) + + return nil + } +} + +func getRMKArtifactMetadata(keyPath string) (*RMKArtifactMetadata, error) { + rmkArtifactMetadata := &RMKArtifactMetadata{} + aws := &aws_provider.AwsConfigure{Region: util.RMKBucketRegion} + data, err := aws.GetAWSBucketFileData(util.RMKBucketName, util.RMKBin+"/"+keyPath+"/metadata.json") + if err != nil { + return nil, err + } + + if err := json.Unmarshal(data, &rmkArtifactMetadata); err != nil { + return nil, err + } + + return rmkArtifactMetadata, nil +} + +func rmkURLFormation(paths ...string) string { + u, err := url.Parse("https://" + util.RMKBucketName + ".s3." + util.RMKBucketRegion + ".amazonaws.com") + if err != nil { + zap.S().Fatal(err) + } + + p := append([]string{u.Path}, paths...) + u.Path = path.Join(p...) + return u.String() +} + +func updateRMK(pkgName, version string, silent, progressBar bool, ctx *cli.Context) error { + zap.S().Infof("starting package download: %s", pkgName) + pkgDst := util.GetHomePath(filepath.Join(".local", util.ToolsBinDir)) + if err := util.DownloadArtifact( + rmkURLFormation(util.RMKBin, version, pkgName), + pkgDst, + pkgName, + &http.Header{}, + silent, + progressBar, + ctx.Context, + ); err != nil { + return err + } + + if err := os.Rename(filepath.Join(pkgDst, pkgName), filepath.Join(pkgDst, util.RMKBin)); err != nil { + return err + } + + if err := os.Chmod(filepath.Join(pkgDst, util.RMKBin), 0755); err != nil { + return err + } + + relPath := strings.ReplaceAll(util.RMKSymLinkPath, filepath.Base(util.RMKSymLinkPath), "") + if syscall.Access(relPath, uint32(2)) == nil { + if !util.IsExists(util.RMKSymLinkPath, true) { + return os.Symlink(filepath.Join(pkgDst, util.RMKBin), util.RMKSymLinkPath) + } + } else { + zap.S().Warnf("symlink was not created automatically due to permissions, "+ + "please complete installation by running command: \n"+ + "sudo ln -s %s %s", filepath.Join(pkgDst, util.RMKBin), util.RMKSymLinkPath) + } + + return nil +} + +func updateAction() cli.ActionFunc { + return func(c *cli.Context) error { + if err := util.ValidateNArg(c, 0); err != nil { + return err + } + + var version string + latestPath := "latest" + + if c.Bool("release-candidate") { + latestPath = "latest-rc" + } + + metadata, err := getRMKArtifactMetadata(latestPath) + if err != nil { + return err + } + + if len(c.String("version")) > 0 { + v, err := semver.NewVersion(c.String("version")) + if err != nil { + return err + } + + version = v.Original() + } + + verCurrent, _ := semver.NewVersion(fmt.Sprintf("%v", c.App.Metadata["version"])) + verFound, _ := semver.NewVersion(metadata.Version) + binaryName := fmt.Sprintf("%s", c.App.Metadata["binaryName"]) + if verCurrent.LessThan(verFound) && len(version) == 0 { + zap.S().Infof("newer release version RMK available: %s", verFound.Original()) + if err := updateRMK(binaryName, latestPath, false, true, c); err != nil { + return err + } + } else if len(version) > 0 { + zap.S().Infof("update current RMK version from %s to %s", + c.App.Metadata["version"], version) + if err := updateRMK(binaryName, version, false, true, c); err != nil { + return err + } + } else { + zap.S().Infof("installed RMK version %s is up-to-date", verCurrent.Original()) + } + + return nil + } +} diff --git a/commands/secret_category.go b/cmd/secret.go similarity index 53% rename from commands/secret_category.go rename to cmd/secret.go index c42385d..a46fd0a 100644 --- a/commands/secret_category.go +++ b/cmd/secret.go @@ -1,4 +1,4 @@ -package commands +package cmd import ( "bytes" @@ -13,7 +13,10 @@ import ( "gopkg.in/yaml.v3" "rmk/config" - "rmk/system" + "rmk/providers/aws_provider" + "rmk/providers/azure_provider" + "rmk/providers/google_provider" + "rmk/util" ) type SecretRunner interface { @@ -40,8 +43,8 @@ func newSecretCommands(conf *config.Config, ctx *cli.Context, workDir string) *S return &SecretCommands{&ReleaseCommands{Conf: conf, Ctx: ctx, WorkDir: workDir}} } -func (sc *SecretCommands) ageKeygen(args ...string) *system.SpecCMD { - return &system.SpecCMD{ +func (sc *SecretCommands) ageKeygen(args ...string) *util.SpecCMD { + return &util.SpecCMD{ Args: args, Command: "age-keygen", Ctx: sc.Ctx.Context, @@ -50,13 +53,13 @@ func (sc *SecretCommands) ageKeygen(args ...string) *system.SpecCMD { } } -func (sc *SecretCommands) helm() *system.SpecCMD { - return &system.SpecCMD{ +func (sc *SecretCommands) helm() *util.SpecCMD { + return &util.SpecCMD{ Args: []string{"secrets"}, Command: "helm", Ctx: sc.Ctx.Context, Dir: sc.WorkDir, - Envs: []string{"SOPS_AGE_KEY_FILE=" + filepath.Join(sc.Conf.SopsAgeKeys, system.SopsAgeKeyFile)}, + Envs: []string{"SOPS_AGE_KEY_FILE=" + filepath.Join(sc.Conf.SopsAgeKeys, util.SopsAgeKeyFile)}, Debug: true, DisableStdOut: true, } @@ -65,29 +68,29 @@ func (sc *SecretCommands) helm() *system.SpecCMD { func (sc *SecretCommands) createAgeKey(scope string) error { keyPath := filepath.Join(sc.Conf.SopsAgeKeys, sc.Conf.Tenant+"-"+scope+".txt") - if system.IsExists(keyPath, true) { + if util.IsExists(keyPath, true) { return fmt.Errorf("key for scope %s exists, if you want to recreate, delete this file %s "+ "and run the command again", scope, keyPath) } sc.SpecCMD = sc.ageKeygen("-o", keyPath) sc.SpecCMD.DisableStdOut = true - if err := runner(sc).runCMD(); err != nil { + if err := releaseRunner(sc).runCMD(); err != nil { return err } sc.SpecCMD = sc.ageKeygen("-y", keyPath) sc.SpecCMD.DisableStdOut = true - return runner(sc).runCMD() + return releaseRunner(sc).runCMD() } func (sc *SecretCommands) CreateKeys() error { - if !system.IsExists(system.GetPwdPath(system.TenantValuesDIR), false) { + if !util.IsExists(util.GetPwdPath(util.TenantValuesDIR), false) { return fmt.Errorf("'%s' directory not exist in project structure, please generate structure "+ - "by running command: 'rmk project generate'", system.TenantValuesDIR) + "by running command: 'rmk project generate'", util.TenantValuesDIR) } - scopes, err := os.ReadDir(system.GetPwdPath(system.TenantValuesDIR)) + scopes, err := os.ReadDir(util.GetPwdPath(util.TenantValuesDIR)) if err != nil { return err } @@ -104,22 +107,22 @@ func (sc *SecretCommands) CreateKeys() error { zap.S().Infof("generate age key for scope: %s", scope.Name()) - sopsConfigFiles, err := system.WalkInDir(system.GetPwdPath(system.TenantValuesDIR, scope.Name()), - "secrets", system.SopsConfigFile) + sopsConfigFiles, err := util.WalkInDir(util.GetPwdPath(util.TenantValuesDIR, scope.Name()), + "secrets", util.SopsConfigFile) if err != nil { return err } if len(sopsConfigFiles) == 0 { - secretSpecFiles, err := system.WalkInDir(system.GetPwdPath(system.TenantValuesDIR, scope.Name()), - "secrets", system.SecretSpecFile) + secretSpecFiles, err := util.WalkInDir(util.GetPwdPath(util.TenantValuesDIR, scope.Name()), + "secrets", util.SecretSpecFile) if err != nil { return err } for _, specFile := range secretSpecFiles { dirSpecFile, _ := filepath.Split(specFile) - sopsConfigFiles = append(sopsConfigFiles, filepath.Join(dirSpecFile, system.SopsConfigFile)) + sopsConfigFiles = append(sopsConfigFiles, filepath.Join(dirSpecFile, util.SopsConfigFile)) } } @@ -150,6 +153,155 @@ func (sc *SecretCommands) CreateKeys() error { return nil } +func (sc *SecretCommands) DownloadKeys() error { + switch sc.Conf.ClusterProvider { + case aws_provider.AWSClusterProvider: + secrets, err := aws_provider.NewAwsConfigure(sc.Ctx.Context, sc.Conf.Profile).GetAWSSecrets(sc.Conf.Tenant) + if err != nil { + return err + } + + if len(secrets) == 0 { + zap.S().Warnf("SOPS Age keys contents for tenant %s not found in AWS Secrets Manager secrets", + sc.Conf.Tenant) + } + + for key, val := range secrets { + zap.S().Infof("download AWS Secrets Manager secret %s to %s", + key, filepath.Join(sc.Conf.SopsAgeKeys, key+util.SopsAgeKeyExt)) + if err := os.WriteFile(filepath.Join(sc.Conf.SopsAgeKeys, key+util.SopsAgeKeyExt), val, 0644); err != nil { + return err + } + } + + return nil + case azure_provider.AzureClusterProvider: + if err := sc.Conf.NewAzureClient(sc.Ctx.Context, sc.Conf.Name); err != nil { + return err + } + + secrets, err := sc.Conf.GetAzureSecrets() + if err != nil { + return err + } + + if len(secrets) == 0 { + zap.S().Warnf("SOPS Age keys contents for tenant %s not found in Azure Key Vault secrets", + sc.Conf.Tenant) + } + + for key, val := range secrets { + zap.S().Infof("download Azure Key Vault secret %s to %s", + key, filepath.Join(sc.Conf.SopsAgeKeys, key+util.SopsAgeKeyExt)) + if err := os.WriteFile(filepath.Join(sc.Conf.SopsAgeKeys, key+util.SopsAgeKeyExt), val, 0644); err != nil { + return err + } + } + + return nil + case google_provider.GoogleClusterProvider: + gcp := google_provider.NewGCPConfigure(sc.Ctx.Context, sc.Conf.GCPConfigure.AppCredentialsPath) + + secrets, err := gcp.GetGCPSecrets(sc.Conf.Tenant) + if err != nil { + return err + } + + if len(secrets) == 0 { + zap.S().Warnf("SOPS Age keys contents for tenant %s not found in GCP Secrets Manager secrets", + sc.Conf.Tenant) + } + + for key, val := range secrets { + zap.S().Infof("download GCP Secrets Manager secret %s to %s", + key, filepath.Join(sc.Conf.SopsAgeKeys, key+util.SopsAgeKeyExt)) + if err := os.WriteFile(filepath.Join(sc.Conf.SopsAgeKeys, key+util.SopsAgeKeyExt), val, 0644); err != nil { + return err + } + } + + return nil + default: + return nil + } +} + +func (sc *SecretCommands) UploadKeys() error { + switch sc.Conf.ClusterProvider { + case aws_provider.AWSClusterProvider: + a := aws_provider.NewAwsConfigure(sc.Ctx.Context, sc.Conf.Profile) + + walkMatch, err := util.WalkMatch(sc.Conf.SopsAgeKeys, sc.Conf.Tenant+"*"+util.SopsAgeKeyExt) + if err != nil { + return err + } + + for _, val := range walkMatch { + file, err := os.ReadFile(val) + if err != nil { + return err + } + + keyName := strings.TrimSuffix(filepath.Base(val), util.SopsAgeKeyExt) + + if err := a.SetAWSSecret(sc.Conf.Tenant, keyName, file); err != nil { + return err + } + } + + return nil + case azure_provider.AzureClusterProvider: + if err := sc.Conf.NewAzureClient(sc.Ctx.Context, sc.Conf.Name); err != nil { + return err + } + + walkMatch, err := util.WalkMatch(sc.Conf.SopsAgeKeys, sc.Conf.Tenant+"*"+util.SopsAgeKeyExt) + if err != nil { + return err + } + + for _, val := range walkMatch { + file, err := os.ReadFile(val) + if err != nil { + return err + } + + keyName := strings.TrimSuffix(filepath.Base(val), util.SopsAgeKeyExt) + value := string(file) + + if err := sc.Conf.SetAzureSecret(keyName, value); err != nil { + return err + } + } + + return nil + case google_provider.GoogleClusterProvider: + gcp := google_provider.NewGCPConfigure(sc.Ctx.Context, sc.Conf.GCPConfigure.AppCredentialsPath) + + walkMatch, err := util.WalkMatch(sc.Conf.SopsAgeKeys, sc.Conf.Tenant+"*"+util.SopsAgeKeyExt) + if err != nil { + return err + } + + for _, val := range walkMatch { + file, err := os.ReadFile(val) + if err != nil { + return err + } + + keyName := strings.TrimSuffix(filepath.Base(val), util.SopsAgeKeyExt) + + if err := gcp.SetGCPSecret(sc.Conf.Tenant, sc.Conf.GCPRegion, keyName, file); err != nil { + return err + } + } + + return nil + default: + return nil + } +} + func (sc *SecretCommands) getOptionFiles(option string) ([]string, error) { var optionFiles, optionPaths []string var check int @@ -157,14 +309,14 @@ func (sc *SecretCommands) getOptionFiles(option string) ([]string, error) { switch { case !sc.Ctx.IsSet("scope") && !sc.Ctx.IsSet("environment"): var err error - optionFiles, err = system.WalkInDir(system.GetPwdPath(system.TenantValuesDIR, filepath.Join(optionPaths...)), + optionFiles, err = util.WalkInDir(util.GetPwdPath(util.TenantValuesDIR, filepath.Join(optionPaths...)), "secrets", option) if err != nil { return nil, err } case sc.Ctx.IsSet("scope") && !sc.Ctx.IsSet("environment"): for _, scope := range sc.Ctx.StringSlice("scope") { - sopsFiles, err := system.WalkInDir(system.GetPwdPath(system.TenantValuesDIR, scope), + sopsFiles, err := util.WalkInDir(util.GetPwdPath(util.TenantValuesDIR, scope), "secrets", option) if err != nil { return nil, err @@ -174,7 +326,7 @@ func (sc *SecretCommands) getOptionFiles(option string) ([]string, error) { } case !sc.Ctx.IsSet("scope") && sc.Ctx.IsSet("environment"): for _, environment := range sc.Ctx.StringSlice("environment") { - for _, env := range sc.Conf.Project.Spec.Environments { + for env := range sc.Conf.Project.Spec.Environments { if environment == env { check++ } @@ -184,7 +336,7 @@ func (sc *SecretCommands) getOptionFiles(option string) ([]string, error) { return nil, fmt.Errorf("environment %s do not exist in project.spec.environments", environment) } - sopsFiles, err := system.WalkInDir(system.GetPwdPath(system.TenantValuesDIR), + sopsFiles, err := util.WalkInDir(util.GetPwdPath(util.TenantValuesDIR), environment, filepath.Join("secrets", option)) if err != nil { return nil, err @@ -196,7 +348,7 @@ func (sc *SecretCommands) getOptionFiles(option string) ([]string, error) { for _, scope := range sc.Ctx.StringSlice("scope") { for _, environment := range sc.Ctx.StringSlice("environment") { optionPaths = append(optionPaths, scope, environment) - sopsFiles, err := system.WalkInDir(system.GetPwdPath(system.TenantValuesDIR, filepath.Join(optionPaths...)), + sopsFiles, err := util.WalkInDir(util.GetPwdPath(util.TenantValuesDIR, filepath.Join(optionPaths...)), "secrets", option) if err != nil { return nil, err @@ -220,14 +372,14 @@ func (sc *SecretCommands) getSecretPaths(optionFiles []string) ([]string, error) } for _, tempPath := range tempPaths { - secrets, err := system.WalkMatch(tempPath, "*.yaml") + secrets, err := util.WalkMatch(tempPath, "*.yaml") if err != nil { return nil, err } for _, secretPath := range secrets { _, file := filepath.Split(secretPath) - if file != system.SopsConfigFile && file != system.SecretSpecFile { + if file != util.SopsConfigFile && file != util.SecretSpecFile { secretPaths = append(secretPaths, secretPath) } } @@ -255,7 +407,7 @@ func (sc *SecretCommands) SecretManager(option string) error { return err } - if err := system.MergeAgeKeys(sc.Conf.SopsAgeKeys); err != nil { + if err := util.MergeAgeKeys(sc.Conf.SopsAgeKeys); err != nil { return err } @@ -265,9 +417,9 @@ func (sc *SecretCommands) SecretManager(option string) error { switch sc.Ctx.Command.Name { case "decrypt": sc.SpecCMD.Args = append(sc.SpecCMD.Args, sc.Ctx.Command.Name, "-i", secret) - if err := runner(sc).runCMD(); err != nil { - if strings.Contains(sc.SpecCMD.StderrBuf.String(), system.HelmSecretsIsNotEncrypted+secret) { - zap.S().Warnf(strings.ToLower(system.HelmSecretsIsNotEncrypted)+"%s", secret) + if err := releaseRunner(sc).runCMD(); err != nil { + if strings.Contains(sc.SpecCMD.StderrBuf.String(), util.HelmSecretsIsNotEncrypted+secret) { + zap.S().Warnf(strings.ToLower(util.HelmSecretsIsNotEncrypted)+"%s", secret) continue } else { return fmt.Errorf(sc.SpecCMD.StderrBuf.String()) @@ -277,9 +429,9 @@ func (sc *SecretCommands) SecretManager(option string) error { zap.S().Infof("decrypting: %s", secret) case "encrypt": sc.SpecCMD.Args = append(sc.SpecCMD.Args, sc.Ctx.Command.Name, "-i", secret) - if err := runner(sc).runCMD(); err != nil { - if strings.Contains(sc.SpecCMD.StderrBuf.String(), system.HelmSecretsAlreadyEncrypted+filepath.Base(secret)) { - zap.S().Warnf(strings.ToLower(system.HelmSecretsAlreadyEncrypted)+"%s", secret) + if err := releaseRunner(sc).runCMD(); err != nil { + if strings.Contains(sc.SpecCMD.StderrBuf.String(), util.HelmSecretsAlreadyEncrypted+filepath.Base(secret)) { + zap.S().Warnf(strings.ToLower(util.HelmSecretsAlreadyEncrypted)+"%s", secret) continue } else { return fmt.Errorf(sc.SpecCMD.StderrBuf.String()) @@ -294,7 +446,7 @@ func (sc *SecretCommands) SecretManager(option string) error { } func (sc *SecretCommands) helmSecretsEncrypt() error { - if err := system.MergeAgeKeys(sc.Conf.SopsAgeKeys); err != nil { + if err := util.MergeAgeKeys(sc.Conf.SopsAgeKeys); err != nil { return err } @@ -305,7 +457,7 @@ func (sc *SecretCommands) helmSecretsEncrypt() error { } func (sc *SecretCommands) helmSecretsDecrypt() error { - if err := system.MergeAgeKeys(sc.Conf.SopsAgeKeys); err != nil { + if err := util.MergeAgeKeys(sc.Conf.SopsAgeKeys); err != nil { return err } @@ -316,7 +468,7 @@ func (sc *SecretCommands) helmSecretsDecrypt() error { } func (sc *SecretCommands) helmSecretsView() error { - if err := system.MergeAgeKeys(sc.Conf.SopsAgeKeys); err != nil { + if err := util.MergeAgeKeys(sc.Conf.SopsAgeKeys); err != nil { return err } @@ -333,7 +485,7 @@ func (sc *SecretCommands) helmSecretsView() error { } func (sc *SecretCommands) helmSecretsEdit() error { - if err := system.MergeAgeKeys(sc.Conf.SopsAgeKeys); err != nil { + if err := util.MergeAgeKeys(sc.Conf.SopsAgeKeys); err != nil { return err } @@ -345,11 +497,11 @@ func (sc *SecretCommands) helmSecretsEdit() error { } func (sc *SecretCommands) runHelmSecretsCMD(secretFilePath string, returnCMDError bool) error { - if !system.IsExists(secretFilePath, true) { + if !util.IsExists(secretFilePath, true) { return fmt.Errorf("file does not exist: %s", secretFilePath) } - if err := runner(sc).runCMD(); err != nil { + if err := releaseRunner(sc).runCMD(); err != nil { if returnCMDError { return err } @@ -357,13 +509,13 @@ func (sc *SecretCommands) runHelmSecretsCMD(secretFilePath string, returnCMDErro out := sc.SpecCMD.StderrBuf.String() // suppress help message of helm secrets - if strings.Contains(out, system.HelpFlagFull) { - return fmt.Errorf(system.UnknownErrorText, "Helm secrets") + if strings.Contains(out, util.HelpFlagFull) { + return fmt.Errorf(util.UnknownErrorText, "Helm secrets") } // remove unneeded text from helm secrets - out = strings.ReplaceAll(out, system.HelmSecretsOutputPrefix, "") - out = strings.ReplaceAll(out, system.HelmSecretsError, "") + out = strings.ReplaceAll(out, util.HelmSecretsOutputPrefix, "") + out = strings.ReplaceAll(out, util.HelmSecretsError, "") out = strings.TrimSpace(out) // make the first letter lowercase @@ -379,83 +531,71 @@ func (sc *SecretCommands) runHelmSecretsCMD(secretFilePath string, returnCMDErro func secretMgrEncryptDecryptAction(conf *config.Config) cli.ActionFunc { return func(c *cli.Context) error { - if err := system.ValidateArtifactModeDefault(c, ""); err != nil { - return err - } - - if err := system.ValidateNArg(c, 0); err != nil { + if err := util.ValidateNArg(c, 0); err != nil { return err } - if err := resolveDependencies(conf.InitConfig(false), c, false); err != nil { + if err := resolveDependencies(conf.InitConfig(), c, false); err != nil { return err } - return newSecretCommands(conf, c, system.GetPwdPath("")).SecretManager(system.SopsConfigFile) + return newSecretCommands(conf, c, util.GetPwdPath("")).SecretManager(util.SopsConfigFile) } } func secretMgrGenerateAction(conf *config.Config) cli.ActionFunc { return func(c *cli.Context) error { - if err := system.ValidateNArg(c, 0); err != nil { + if err := util.ValidateNArg(c, 0); err != nil { return err } - return newSecretCommands(conf, c, system.GetPwdPath("")).SecretManager(system.SecretSpecFile) + return newSecretCommands(conf, c, util.GetPwdPath("")).SecretManager(util.SecretSpecFile) } } func secretKeysCreateAction(conf *config.Config) cli.ActionFunc { return func(c *cli.Context) error { - if err := system.ValidateArtifactModeDefault(c, ""); err != nil { + if err := util.ValidateNArg(c, 0); err != nil { return err } - if err := system.ValidateNArg(c, 0); err != nil { + if err := resolveDependencies(conf.InitConfig(), c, false); err != nil { return err } - if err := resolveDependencies(conf.InitConfig(false), c, false); err != nil { - return err - } - - return newSecretCommands(conf, c, system.GetPwdPath("")).CreateKeys() + return newSecretCommands(conf, c, util.GetPwdPath("")).CreateKeys() } } func secretKeysDownloadAction(conf *config.Config) cli.ActionFunc { return func(c *cli.Context) error { - if err := system.ValidateNArg(c, 0); err != nil { + if err := util.ValidateNArg(c, 0); err != nil { return err } - return conf.DownloadFromBucket("", conf.SopsBucketName, conf.SopsAgeKeys, conf.Tenant) + return newSecretCommands(conf, c, util.GetPwdPath("")).DownloadKeys() } } func secretKeysUploadAction(conf *config.Config) cli.ActionFunc { return func(c *cli.Context) error { - if err := system.ValidateNArg(c, 0); err != nil { + if err := util.ValidateNArg(c, 0); err != nil { return err } - return conf.UploadToBucket(conf.SopsBucketName, conf.SopsAgeKeys, "*"+system.SopsAgeKeyExt) + return newSecretCommands(conf, c, util.GetPwdPath("")).UploadKeys() } } func secretAction(conf *config.Config, action func(secretRunner SecretRunner) error) cli.ActionFunc { return func(c *cli.Context) error { - if err := system.ValidateArtifactModeDefault(c, ""); err != nil { - return err - } - - if err := system.ValidateNArg(c, 1); err != nil { + if err := util.ValidateNArg(c, 1); err != nil { return err } - if err := resolveDependencies(conf.InitConfig(false), c, false); err != nil { + if err := resolveDependencies(conf.InitConfig(), c, false); err != nil { return err } - return action(newSecretCommands(conf, c, system.GetPwdPath(""))) + return action(newSecretCommands(conf, c, util.GetPwdPath(""))) } } diff --git a/commands/secret_generation_category.go b/cmd/secret_generation.go similarity index 96% rename from commands/secret_generation_category.go rename to cmd/secret_generation.go index 42c10fb..d9515a0 100644 --- a/commands/secret_generation_category.go +++ b/cmd/secret_generation.go @@ -1,4 +1,4 @@ -package commands +package cmd import ( "bytes" @@ -13,7 +13,7 @@ import ( "golang.org/x/crypto/ssh/terminal" "gopkg.in/yaml.v3" - "rmk/system" + "rmk/util" ) // Custom name function for parsing template @@ -99,7 +99,7 @@ func (gf *GenerationFuncMap) renderSpecTemplate(s string, data ...interface{}) e func (g *GenerationSpec) writeSpecSecrets(force bool) error { for _, rule := range g.GenerationRules { - if system.IsExists(filepath.Join(g.secretsDir, rule.Name+".yaml"), true) && !force { + if util.IsExists(filepath.Join(g.secretsDir, rule.Name+".yaml"), true) && !force { zap.S().Warnf("%s exists, new secret generation was skipped", filepath.Join(g.secretsDir, rule.Name+".yaml")) continue diff --git a/commands/cluster_category.go b/commands/cluster_category.go deleted file mode 100644 index 454ba39..0000000 --- a/commands/cluster_category.go +++ /dev/null @@ -1,529 +0,0 @@ -package commands - -import ( - "fmt" - "os" - "path/filepath" - "strings" - - "github.com/urfave/cli/v2" - "go.uber.org/zap" - - "rmk/config" - "rmk/system" -) - -type StateRunner interface { - clusterStateDelete() error - clusterStateList() error - clusterStateRefresh() error -} - -type ClusterCommands struct { - Conf *config.Config - Ctx *cli.Context - SpecCMDs []*system.SpecCMD - PlanFile string - WorkDir string -} - -func (cc *ClusterCommands) clusterRootDir() (string, error) { - for _, provider := range cc.Conf.Clusters { - if strings.HasPrefix(provider.Name, cc.Conf.ClusterProvider) { - return provider.DstPath, nil - } - } - - return "", fmt.Errorf("destination path for cluster provider %s not found", cc.Conf.ClusterProvider) -} - -func (cc *ClusterCommands) awsEks() *system.SpecCMD { - return &system.SpecCMD{ - Args: []string{"eks", "--region", - cc.Conf.Region, - "update-kubeconfig", - "--name", - cc.Conf.Name + "-eks", - "--profile", - cc.Conf.Profile, - }, - Command: "aws", - Ctx: cc.Ctx.Context, - Dir: cc.WorkDir, - Debug: true, - } -} - -func (cc *ClusterCommands) runBatchCMD() error { - if err := os.Unsetenv("AWS_PROFILE"); err != nil { - return err - } - - for _, val := range cc.SpecCMDs { - val.Envs = []string{ - "AWS_PROFILE=" + cc.Conf.Profile, - "AWS_CONFIG_FILE=" + strings.Join(cc.Conf.AWSSharedConfigFile(cc.Conf.Profile), ""), - "AWS_SHARED_CREDENTIALS_FILE=" + strings.Join(cc.Conf.AWSSharedCredentialsFile(cc.Conf.Profile), ""), - } - if err := val.AddEnv(); err != nil { - return err - } - - if err := val.ExecCMD(); err != nil { - if val.Debug { - zap.S().Debugf("command: %s", val.CommandStr) - zap.S().Debugf("path: %s", val.Dir) - for _, v := range val.Envs { - zap.S().Debugf("env: %s", v) - } - } - - return err - } - - if val.Debug { - zap.S().Debugf("command: %s", val.CommandStr) - zap.S().Debugf("path: %s", val.Dir) - for _, v := range val.Envs { - zap.S().Debugf("env: %s", v) - } - } - } - - return nil -} - -func (cc *ClusterCommands) initialize() *system.SpecCMD { - args := []string{ - "init", - "-backend=true", - "-backend-config=region=" + cc.Conf.Region, - "-backend-config=bucket=" + cc.Conf.Terraform.BucketName, - "-backend-config=key=" + cc.Conf.Terraform.BucketKey, - } - if cc.Conf.ClusterProvisionerSL { - args = append(args, "-backend-config=dynamodb_table="+cc.Conf.Terraform.DDBTableName) - } - - args = append(args, "-reconfigure") - return &system.SpecCMD{ - Args: args, - Command: "terraform", - Ctx: cc.Ctx.Context, - Dir: cc.WorkDir, - Debug: true, - } -} - -func (cc *ClusterCommands) validate() *system.SpecCMD { - return &system.SpecCMD{ - Args: []string{"validate"}, - Command: "terraform", - Ctx: cc.Ctx.Context, - Dir: cc.WorkDir, - Debug: true, - } -} - -func (cc *ClusterCommands) workspace(args ...string) *system.SpecCMD { - return &system.SpecCMD{ - Args: append([]string{"workspace"}, args...), - Command: "terraform", - Ctx: cc.Ctx.Context, - Dir: cc.WorkDir, - Debug: true, - } -} - -func (cc *ClusterCommands) output(args ...string) *system.SpecCMD { - return &system.SpecCMD{ - Args: append([]string{"output"}, args...), - Command: "terraform", - Ctx: cc.Ctx.Context, - Dir: cc.WorkDir, - DisableStdOut: true, - Debug: false, - } -} - -func (cc *ClusterCommands) destroy() *system.SpecCMD { - return &system.SpecCMD{ - Args: []string{"destroy", "-auto-approve", - "-var=aws_account_id=" + cc.Conf.AccountID, - "-var=cloudflare_api_token=" + cc.Conf.CloudflareToken, - "-var=name=" + cc.Conf.Name, - "-var=region=" + cc.Conf.Region, - "-var=root_domain=" + cc.Conf.RootDomain, - "-var=terraform_bucket_key=" + cc.Conf.Terraform.BucketKey, - "-var=terraform_bucket_name=" + cc.Conf.Terraform.BucketName, - }, - Command: "terraform", - Ctx: cc.Ctx.Context, - Dir: cc.WorkDir, - Debug: true, - } -} - -func (cc *ClusterCommands) plan() *system.SpecCMD { - return &system.SpecCMD{ - Args: []string{ - "plan", - "-out=" + cc.PlanFile, - "-var=aws_account_id=" + cc.Conf.AccountID, - "-var=cloudflare_api_token=" + cc.Conf.CloudflareToken, - "-var=name=" + cc.Conf.Name, - "-var=region=" + cc.Conf.Region, - "-var=root_domain=" + cc.Conf.RootDomain, - "-var=terraform_bucket_key=" + cc.Conf.Terraform.BucketKey, - "-var=terraform_bucket_name=" + cc.Conf.Terraform.BucketName, - }, - Command: "terraform", - Ctx: cc.Ctx.Context, - Dir: cc.WorkDir, - Debug: true, - } -} - -func (cc *ClusterCommands) apply() *system.SpecCMD { - return &system.SpecCMD{ - Args: []string{"apply", cc.PlanFile}, - Command: "terraform", - Ctx: cc.Ctx.Context, - Dir: cc.WorkDir, - Debug: true, - } -} - -func (cc *ClusterCommands) listResources() *system.SpecCMD { - return &system.SpecCMD{ - Args: []string{"state", "list"}, - Command: "terraform", - Ctx: cc.Ctx.Context, - Dir: cc.WorkDir, - Debug: true, - } -} - -func (cc *ClusterCommands) refresh() *system.SpecCMD { - return &system.SpecCMD{ - Args: []string{"refresh", - "-var=aws_account_id=" + cc.Conf.AccountID, - "-var=cloudflare_api_token=" + cc.Conf.CloudflareToken, - "-var=name=" + cc.Conf.Name, - "-var=region=" + cc.Conf.Region, - "-var=root_domain=" + cc.Conf.RootDomain, - "-var=terraform_bucket_key=" + cc.Conf.Terraform.BucketKey, - "-var=terraform_bucket_name=" + cc.Conf.Terraform.BucketName, - }, - Command: "terraform", - Ctx: cc.Ctx.Context, - Dir: cc.WorkDir, - Debug: true, - } -} - -func (cc *ClusterCommands) state(args ...string) *system.SpecCMD { - return &system.SpecCMD{ - Args: append([]string{"state"}, args...), - Command: "terraform", - Ctx: cc.Ctx.Context, - Dir: cc.WorkDir, - Debug: true, - } -} - -func (cc *ClusterCommands) clusterContext() error { - cc.SpecCMDs = append(cc.SpecCMDs, cc.awsEks()) - return cc.runBatchCMD() -} - -func (cc *ClusterCommands) clusterDestroy() error { - checkWorkspace, err := cc.Conf.BucketKeyExists("", cc.Conf.Terraform.BucketName, "env:/"+cc.Conf.Name+"/tf.tfstate") - if err != nil { - return err - } - - if checkWorkspace { - cc.SpecCMDs = append(cc.SpecCMDs, cc.initialize(), cc.validate(), cc.workspace("select", cc.Conf.Name)) - if err := cc.runBatchCMD(); err != nil { - return err - } - - destroy := cc.destroy() - - match, err := system.WalkMatch( - system.GetPwdPath(system.TenantValuesDIR, "clusters", system.AWSClusterProvider, cc.Conf.Environment), - "*."+system.TerraformVarsExt, - ) - if err != nil { - return err - } - - for _, val := range match { - destroy.Args = append(destroy.Args, "-var-file="+val) - } - - if err := os.RemoveAll(cc.PlanFile); err != nil { - return err - } - - cc.SpecCMDs = append([]*system.SpecCMD{}, destroy, cc.workspace("select", "default"), - cc.workspace("delete", cc.Conf.Name)) - - return cc.runBatchCMD() - } else { - zap.S().Infof("Terraform cluster in workspace %s already deleted or not created", cc.Conf.Name) - return nil - } -} - -func (cc *ClusterCommands) clusterList() error { - cc.SpecCMDs = append(cc.SpecCMDs, cc.initialize(), cc.workspace("list")) - return cc.runBatchCMD() -} - -func (cc *ClusterCommands) clusterProvision() error { - var workspace *system.SpecCMD - - if err := os.MkdirAll(filepath.Join(cc.WorkDir, "plans"), 0755); err != nil { - zap.S().Fatal(err) - } - - checkWorkspace, err := cc.Conf.BucketKeyExists("", cc.Conf.Terraform.BucketName, "env:/"+cc.Conf.Name+"/tf.tfstate") - if err != nil { - zap.S().Fatal(err) - } - - if checkWorkspace { - workspace = cc.workspace("select", cc.Conf.Name) - } else { - workspace = cc.workspace("new", cc.Conf.Name) - } - - plan := cc.plan() - - match, err := system.WalkMatch( - system.GetPwdPath(system.TenantValuesDIR, "clusters", system.AWSClusterProvider, cc.Conf.Environment), - "*."+system.TerraformVarsExt, - ) - - for _, val := range match { - plan.Args = append(plan.Args, "-var-file="+val) - } - - if cc.Ctx.Bool("plan") { - cc.SpecCMDs = append(cc.SpecCMDs, cc.initialize(), cc.validate(), workspace, plan) - return cc.runBatchCMD() - } - - cc.SpecCMDs = append(cc.SpecCMDs, cc.initialize(), cc.validate(), workspace, plan, cc.apply()) - if err := cc.runBatchCMD(); err != nil { - return err - } - - rc := &ReleaseCommands{ - Conf: cc.Conf, - Ctx: cc.Ctx, - WorkDir: system.GetPwdPath(""), - UpdateContext: true, - } - - return rc.releaseKubeContext() -} - -func (cc *ClusterCommands) clusterStateDelete() error { - cc.SpecCMDs = append(cc.SpecCMDs, cc.state("rm", cc.Ctx.String("resource-address"))) - - return cc.runBatchCMD() -} - -func (cc *ClusterCommands) clusterStateList() error { - cc.SpecCMDs = append(cc.SpecCMDs, cc.state(cc.Ctx.Command.Name)) - - return cc.runBatchCMD() -} - -func (cc *ClusterCommands) clusterStateRefresh() error { - var workspace *system.SpecCMD - - checkWorkspace, err := cc.Conf.BucketKeyExists("", cc.Conf.Terraform.BucketName, "env:/"+cc.Conf.Name+"/tf.tfstate") - if err != nil { - zap.S().Fatal(err) - } - - if checkWorkspace { - workspace = cc.workspace("select", cc.Conf.Name) - } else { - workspace = cc.workspace("new", cc.Conf.Name) - } - - refresh := cc.refresh() - match, err := system.WalkMatch( - system.GetPwdPath(system.TenantValuesDIR, "clusters", system.AWSClusterProvider, cc.Conf.Environment), - "*."+system.TerraformVarsExt, - ) - if err != nil { - return err - } - - for _, val := range match { - refresh.Args = append(refresh.Args, "-var-file="+val) - } - - cc.SpecCMDs = append(cc.SpecCMDs, cc.initialize(), cc.validate(), workspace, refresh) - - return cc.runBatchCMD() -} - -func clusterDestroyAction(conf *config.Config) cli.ActionFunc { - return func(c *cli.Context) error { - if err := system.ValidateArtifactModeDefault(c, ""); err != nil { - return err - } - - if err := system.ValidateNArg(c, 0); err != nil { - return err - } - - if err := resolveDependencies(conf.InitConfig(false), c, false); err != nil { - return err - } - - cc := &ClusterCommands{ - Conf: conf, - Ctx: c, - } - - if pkgDst, err := cc.clusterRootDir(); err != nil { - return err - } else { - cc.WorkDir = filepath.Join(pkgDst, "terraform") - } - - cc.PlanFile = filepath.Join(cc.WorkDir, "plans", conf.Name+"__"+conf.Environment+".tfplan") - - return cc.clusterDestroy() - } -} - -func clusterListAction(conf *config.Config) cli.ActionFunc { - return func(c *cli.Context) error { - if err := system.ValidateArtifactModeDefault(c, ""); err != nil { - return err - } - - if err := system.ValidateNArg(c, 0); err != nil { - return err - } - - if err := resolveDependencies(conf.InitConfig(false), c, false); err != nil { - return err - } - - cc := &ClusterCommands{ - Conf: conf, - Ctx: c, - } - - if pkgDst, err := cc.clusterRootDir(); err != nil { - return err - } else { - cc.WorkDir = filepath.Join(pkgDst, "terraform") - } - - return cc.clusterList() - } -} - -func clusterProvisionAction(conf *config.Config) cli.ActionFunc { - return func(c *cli.Context) error { - if err := system.ValidateArtifactModeDefault(c, ""); err != nil { - return err - } - - if err := system.ValidateNArg(c, 0); err != nil { - return err - } - - if err := resolveDependencies(conf.InitConfig(true), c, false); err != nil { - return err - } - - cc := &ClusterCommands{ - Conf: conf, - Ctx: c, - } - - if pkgDst, err := cc.clusterRootDir(); err != nil { - return err - } else { - cc.WorkDir = filepath.Join(pkgDst, "terraform") - } - - cc.PlanFile = filepath.Join(cc.WorkDir, "plans", conf.Name+"__"+conf.Environment+".tfplan") - - if err := cc.clusterProvision(); err != nil { - return err - } - - if err := conf.GetTerraformOutputs(); err != nil { - return err - } - - return conf.CreateConfigFile() - } -} - -func clusterStateAction(conf *config.Config, action func(stateRunner StateRunner) error) cli.ActionFunc { - return func(c *cli.Context) error { - if err := system.ValidateArtifactModeDefault(c, ""); err != nil { - return err - } - - if err := system.ValidateNArg(c, 0); err != nil { - return err - } - - if err := resolveDependencies(conf.InitConfig(false), c, false); err != nil { - return err - } - - cc := &ClusterCommands{ - Conf: conf, - Ctx: c, - } - - if pkgDst, err := cc.clusterRootDir(); err != nil { - return err - } else { - cc.WorkDir = filepath.Join(pkgDst, "terraform") - } - - return action(cc) - } -} - -func clusterSwitchAction(conf *config.Config) cli.ActionFunc { - return func(c *cli.Context) error { - if err := system.ValidateArtifactModeDefault(c, ""); err != nil { - return err - } - - if err := system.ValidateNArg(c, 0); err != nil { - return err - } - - if err := resolveDependencies(conf.InitConfig(false), c, false); err != nil { - return err - } - - rc := &ReleaseCommands{ - Conf: conf, - Ctx: c, - WorkDir: system.GetPwdPath(""), - UpdateContext: c.Bool("force"), - } - - return rc.releaseKubeContext() - } -} diff --git a/commands/config_category.go b/commands/config_category.go deleted file mode 100644 index fd6b22c..0000000 --- a/commands/config_category.go +++ /dev/null @@ -1,602 +0,0 @@ -package commands - -import ( - "encoding/json" - "fmt" - "os" - "strconv" - "strings" - "time" - - "github.com/Masterminds/semver" - "github.com/urfave/cli/v2" - "go.uber.org/zap" - - "rmk/config" - "rmk/git_handler" - "rmk/system" -) - -type ConfigCommands struct { - *ReleaseCommands -} - -func newConfigCommands(conf *config.Config, ctx *cli.Context, workDir string) *ConfigCommands { - return &ConfigCommands{&ReleaseCommands{Conf: conf, Ctx: ctx, WorkDir: workDir}} -} - -func (c *ConfigCommands) awsConfigure(profile string) *system.SpecCMD { - return &system.SpecCMD{ - Args: []string{"configure", "--profile", profile}, - Envs: []string{ - "AWS_CONFIG_FILE=" + strings.Join(c.Conf.AWSSharedConfigFile(profile), ""), - "AWS_SHARED_CREDENTIALS_FILE=" + strings.Join(c.Conf.AWSSharedCredentialsFile(profile), ""), - }, - Command: "aws", - Ctx: c.Ctx.Context, - Dir: c.WorkDir, - Debug: false, - } -} - -func (c *ConfigCommands) helmPlugin() *system.SpecCMD { - return &system.SpecCMD{ - Args: []string{"plugin"}, - Command: "helm", - Dir: c.WorkDir, - Ctx: c.Ctx.Context, - DisableStdOut: true, - Debug: false, - } -} - -func (c *ConfigCommands) rmkConfigInit() *system.SpecCMD { - exRMK, err := os.Executable() - if err != nil { - panic(err) - } - - return &system.SpecCMD{ - Args: []string{"config", "init"}, - Command: exRMK, - Dir: c.WorkDir, - Ctx: c.Ctx.Context, - Debug: true, - } -} - -func (c *ConfigCommands) checkAwsEnv() (map[string]string, bool) { - awsEnvs := map[string]string{ - "region": "AWS_REGION", - "aws_access_key_id": "AWS_ACCESS_KEY_ID", - "aws_secret_access_key": "AWS_SECRET_ACCESS_KEY", - "aws_session_token": "AWS_SESSION_TOKEN", - } - - for key, val := range awsEnvs { - value, ok := os.LookupEnv(val) - if !ok { - delete(awsEnvs, key) - } else { - awsEnvs[key] = value - } - } - - if len(awsEnvs) > 0 { - return awsEnvs, true - } else { - return nil, false - } -} - -func (c *ConfigCommands) configAws() error { - if awsEnvs, ok := c.checkAwsEnv(); !ok { - c.SpecCMD = c.awsConfigure(c.Conf.Profile) - return runner(c).runCMD() - } else { - for key, val := range awsEnvs { - c.SpecCMD = c.awsConfigure(c.Conf.Profile) - c.SpecCMD.Args = append(c.SpecCMD.Args, "set", key, val) - if err := runner(c).runCMD(); err != nil { - return err - } - } - - zap.S().Infof("AWS profile by name %s was created", c.Conf.Profile) - return nil - } -} - -func (c *ConfigCommands) configAwsMFA() error { - var tokenExpiration time.Time - currentTime := time.Now() - regularProfile := c.Conf.Profile - - if len(c.Conf.AWSMFATokenExpiration) > 0 { - unixTime, err := strconv.ParseInt(c.Conf.AWSMFATokenExpiration, 10, 64) - if err != nil { - return err - } - - tokenExpiration = time.Unix(unixTime, 0) - } - - if len(c.Conf.AWSMFAProfile) > 0 { - c.Conf.AwsConfigure.Profile = c.Conf.AWSMFAProfile - } - - if err := c.Conf.GetMFADevicesSerialNumbers(); err != nil { - return err - } - - timeDiff := time.Time{}.Add(tokenExpiration.Sub(currentTime)).Format("15:04:05") - - if len(c.Conf.MFADeviceSerialNumber) > 0 { - zap.S().Infof("MFA device SerialNumber: %s", c.Conf.MFADeviceSerialNumber) - } - - if currentTime.Before(tokenExpiration) { - zap.S().Infof("MFA remaining time for token validity: %s", timeDiff) - } - - if len(c.Conf.MFADeviceSerialNumber) > 0 && currentTime.After(tokenExpiration) { - if err := c.Conf.GetMFASessionToken(); err != nil { - return err - } - - c.Conf.AWSMFAProfile = regularProfile + "-mfa" - c.Conf.AWSMFATokenExpiration = strconv.FormatInt(c.Conf.Expiration.Unix(), 10) - - MFAProfileArgs := map[string]string{ - "aws_access_key_id": c.Conf.MFAProfileCredentials.AccessKeyID, - "aws_secret_access_key": c.Conf.MFAProfileCredentials.SecretAccessKey, - "output": "text", - "region": c.Conf.Region, - } - - regularProfileArgs := map[string]string{ - "aws_access_key_id": c.Conf.MFAToken.AccessKeyId, - "aws_secret_access_key": c.Conf.MFAToken.SecretAccessKey, - "aws_session_token": c.Conf.MFAToken.SessionToken, - } - - for key, val := range MFAProfileArgs { - c.SpecCMD = c.awsConfigure(c.Conf.AWSMFAProfile) - c.SpecCMD.Args = append(c.SpecCMD.Args, "set", key, val) - if err := runner(c).runCMD(); err != nil { - return err - } - } - - for key, val := range regularProfileArgs { - c.SpecCMD = c.awsConfigure(regularProfile) - c.SpecCMD.Args = append(c.SpecCMD.Args, "set", key, val) - if err := runner(c).runCMD(); err != nil { - return err - } - } - } - - c.Conf.AwsConfigure.Profile = regularProfile - - return nil -} - -func (c *ConfigCommands) copyAWSProfile(profile string) error { - profileArgs := map[string]string{ - "aws_access_key_id": c.Conf.MFAProfileCredentials.AccessKeyID, - "aws_secret_access_key": c.Conf.MFAProfileCredentials.SecretAccessKey, - "output": "text", - "region": c.Conf.Region, - } - - for key, val := range profileArgs { - c.SpecCMD = c.awsConfigure(profile) - c.SpecCMD.Args = append(c.SpecCMD.Args, "set", key, val) - if err := runner(c).runCMD(); err != nil { - return err - } - } - - return nil -} - -func (c *ConfigCommands) uninstallHelmPlugin(plugin config.Package) error { - c.SpecCMD = c.helmPlugin() - c.SpecCMD.Args = append(c.SpecCMD.Args, "list") - plSemVer, _ := semver.NewVersion(plugin.Version) - - if err := runner(c).runCMD(); err != nil { - return fmt.Errorf("get Helm plugin list failed: %s", c.SpecCMD.StderrBuf.String()) - } - - for _, v := range strings.Split(c.SpecCMD.StdoutBuf.String(), "\n") { - if strings.Contains(v, plugin.Name) && !strings.Contains(v, plSemVer.String()) { - zap.S().Infof("Helm plugin %s detect new version %s from config", plugin.Name, plugin.Version) - c.SpecCMD = c.helmPlugin() - c.SpecCMD.Args = append(c.SpecCMD.Args, "uninstall", plugin.Name) - if err := runner(c).runCMD(); err != nil { - return fmt.Errorf("Helm plugin %s uninstallation failed: \n%s", - plugin.Name, c.SpecCMD.StderrBuf.String()) - } - - break - } - } - - return nil -} - -func (c *ConfigCommands) installHelmPlugin(plugin config.Package, args ...string) error { - c.SpecCMD = c.helmPlugin() - c.SpecCMD.Args = append(c.SpecCMD.Args, args...) - if err := runner(c).runCMD(); err != nil { - if !strings.Contains(c.SpecCMD.StderrBuf.String(), system.HelmPluginExist) { - return fmt.Errorf("Helm plugin %s installation failed: \n%s", plugin.Name, c.SpecCMD.StderrBuf.String()) - } - } - - if !strings.Contains(c.SpecCMD.StderrBuf.String(), system.HelmPluginExist) { - zap.S().Infof("installing Helm plugin: %s", plugin.Name) - } - - return nil -} - -func (c *ConfigCommands) configHelmPlugins() error { - for _, plugin := range c.Conf.HelmPlugins { - if err := c.uninstallHelmPlugin(*plugin); err != nil { - return err - } - - if err := c.installHelmPlugin(*plugin, "install", plugin.Url, "--version="+plugin.Version); err != nil { - return err - } - } - - return nil -} - -func (c *ConfigCommands) rmkConfig() error { - c.SpecCMD = c.rmkConfigInit() - return runner(c).runCMD() -} - -func initAWSProfile(c *cli.Context, conf *config.Config, gitSpec *git_handler.GitSpec) error { - var profile string - - // Detect if MFA is enabled - if len(conf.AWSMFAProfile) > 0 && len(conf.AWSMFATokenExpiration) > 0 { - profile = conf.AWSMFAProfile - } else { - profile = conf.Profile - } - - if c.Bool("aws-reconfigure") { - if err := os.RemoveAll(strings.Join(conf.AWSSharedCredentialsFile(conf.Profile), "")); err != nil { - return err - } - - // Reconfigure regular AWS profile - if err := newConfigCommands(conf, c, system.GetPwdPath("")).configAws(); err != nil { - return err - } - - // Get CallerIdentity and region for regular AWS profile - if _, err := conf.AwsConfigure.GetAwsConfigure(conf.Profile); err != nil { - return err - } - - // Delete MFA profile - if strings.Contains(profile, "-mfa") { - if err := os.RemoveAll(strings.Join(conf.AWSSharedConfigFile(profile), "")); err != nil { - return err - } - - if err := os.RemoveAll(strings.Join(conf.AWSSharedCredentialsFile(profile), "")); err != nil { - return err - } - } - - // Reset ConfigFrom value for config for current environment - conf.ConfigFrom = gitSpec.ID - // Reset AWSMFAProfile value for config for current environment - conf.AWSMFAProfile = "" - // Reset AWSMFATokenExpiration value for config for current environment - conf.AWSMFATokenExpiration = "" - // Returning a regular profile value - profile = conf.Profile - - // Create new MFA profile - if err := newConfigCommands(conf, c, system.GetPwdPath("")).configAwsMFA(); err != nil { - return err - } - } - - if ok, err := conf.AwsConfigure.GetAwsConfigure(profile); err != nil && ok { - zap.S().Warnf("%s", err.Error()) - if err := newConfigCommands(conf, c, system.GetPwdPath("")).configAws(); err != nil { - return err - } - - if _, err := conf.AwsConfigure.GetAwsConfigure(profile); err != nil { - return err - } - - if err := newConfigCommands(conf, c, system.GetPwdPath("")).configAwsMFA(); err != nil { - return err - } - } else if !c.Bool("aws-reconfigure") { - if err := newConfigCommands(conf, c, system.GetPwdPath("")).configAwsMFA(); err != nil { - return err - } - } else if !ok && err != nil { - return err - } - - return nil -} - -func getConfigFromEnvironment(c *cli.Context, conf *config.Config, gitSpec *git_handler.GitSpec) error { - if len(c.String("config-from-environment")) > 0 { - configPath := system.GetHomePath(system.RMKDir, system.RMKConfig, - gitSpec.RepoPrefixName+"-"+c.String("config-from-environment")+".yaml") - - if err := conf.ReadConfigFile(configPath); err != nil { - zap.S().Errorf("RMK config %s.yaml not initialized, please checkout to branch %s "+ - "and run command 'rmk config init' with specific parameters", - c.String("config-from-environment"), c.String("config-from-environment")) - return err - } - - if err := c.Set("config-from", conf.Name); err != nil { - return err - } - - // Delete regular profile - if err := os.RemoveAll(strings.Join(conf.AWSSharedCredentialsFile(gitSpec.ID), "")); err != nil { - return err - } - - if len(conf.AWSMFAProfile) > 0 && len(conf.AWSMFATokenExpiration) > 0 { - regularProfile := conf.Profile - - // Get MFA profile credentials. - conf.AwsConfigure.Profile = conf.AWSMFAProfile - if err := conf.GetAWSCredentials(); err != nil { - return err - } - - // Copy MFA profile for current environment - conf.AwsConfigure.Profile = regularProfile - if err := newConfigCommands(conf, c, system.GetPwdPath("")).copyAWSProfile(gitSpec.ID); err != nil { - return err - } - } else { - // Delete config MFA profile - if err := os.RemoveAll(strings.Join(conf.AWSSharedConfigFile(gitSpec.ID+"-mfa"), "")); err != nil { - return err - } - - // Delete credentials MFA profile - if err := os.RemoveAll(strings.Join(conf.AWSSharedCredentialsFile(gitSpec.ID+"-mfa"), "")); err != nil { - return err - } - - // Get regular profile credentials. - if err := conf.GetAWSCredentials(); err != nil { - return err - } - - // Copy regular profile for current environment - if err := newConfigCommands(conf, c, system.GetPwdPath("")).copyAWSProfile(gitSpec.ID); err != nil { - return err - } - } - - // Reset AWSMFAProfile value for config for current environment - if err := c.Set("aws-mfa-profile", ""); err != nil { - return err - } - - // Reset AWSMFATokenExpiration value for config for current environment - if err := c.Set("aws-mfa-token-expiration", ""); err != nil { - return err - } - - conf.ConfigFrom = c.String("config-from") - conf.AwsConfigure.Profile = gitSpec.ID - - return nil - } - - if err := system.ValidateArtifactModeDefault(c, "required parameter --github-token not set"); err != nil { - return err - } - - if err := system.ValidateNArg(c, 0); err != nil { - return err - } - - if !c.IsSet("config-from") { - if err := c.Set("config-from", gitSpec.ID); err != nil { - return err - } - } - - conf.ConfigFrom = c.String("config-from") - conf.AwsConfigure.Profile = gitSpec.ID - conf.CloudflareToken = c.String("cloudflare-token") - conf.GitHubToken = c.String("github-token") - - return nil -} - -func configDeleteAction(conf *config.Config) cli.ActionFunc { - return func(c *cli.Context) error { - if err := system.ValidateNArg(c, 0); err != nil { - return err - } - - // Delete MFA profile - if len(conf.AWSMFAProfile) > 0 && len(conf.AWSMFATokenExpiration) > 0 { - if err := os.RemoveAll(strings.Join(conf.AWSSharedConfigFile(conf.AWSMFAProfile), "")); err != nil { - return err - } - - if err := os.RemoveAll(strings.Join(conf.AWSSharedCredentialsFile(conf.AWSMFAProfile), "")); err != nil { - return err - } - } - - // Delete config MFA profile - if err := os.RemoveAll(strings.Join(conf.AWSSharedConfigFile(conf.Profile), "")); err != nil { - return err - } - - // Delete credentials MFA profile - if err := os.RemoveAll(strings.Join(conf.AWSSharedCredentialsFile(conf.Profile), "")); err != nil { - return err - } - - if err := os.RemoveAll(c.String("config")); err != nil { - return err - } - - zap.S().Infof("deleted config file by path: %s", c.String("config")) - - return nil - } -} - -func configInitAction(conf *config.Config, gitSpec *git_handler.GitSpec) cli.ActionFunc { - return func(c *cli.Context) error { - if err := getConfigFromEnvironment(c, conf, gitSpec); err != nil { - return err - } - - zap.S().Infof("loaded config file by path: %s", c.String("config")) - - start := time.Now() - - conf.Name = gitSpec.ID - conf.Tenant = gitSpec.RepoPrefixName - conf.Environment = gitSpec.DefaultBranch - zap.S().Infof("RMK will use values for %s environment", conf.Environment) - - if c.Bool("slack-notifications") { - conf.SlackNotifications = c.Bool("slack-notifications") - if !c.IsSet("slack-webhook") || !c.IsSet("slack-channel") { - return fmt.Errorf("parameters --slack-webhook, --slack-channel not set, " + - "required if Slack notifications are enabled") - } else { - conf.SlackWebHook = c.String("slack-webhook") - conf.SlackChannel = c.String("slack-channel") - conf.SlackMsgDetails = c.StringSlice("slack-message-details") - } - } - - conf.ArtifactMode = c.String("artifact-mode") - conf.ProgressBar = c.Bool("progress-bar") - conf.Terraform.BucketKey = system.TenantBucketKey - conf.ClusterProvisionerSL = c.Bool("cluster-provisioner-state-locking") - conf.S3ChartsRepoRegion = c.String("s3-charts-repo-region") - conf.ClusterProvider = c.String("cluster-provider") - conf.AWSMFAProfile = c.String("aws-mfa-profile") - conf.AWSMFATokenExpiration = c.String("aws-mfa-token-expiration") - conf.AWSECRHost = c.String("aws-ecr-host") - conf.AWSECRRegion = c.String("aws-ecr-region") - conf.AWSECRUserName = c.String("aws-ecr-user-name") - - // AWS Profile init configuration with support MFA - if err := initAWSProfile(c, conf, gitSpec); err != nil { - return err - } - - //Formation of a unique bucket name, consisting of the prefix tenant of the repository, - //constant and the first 3 and last 2 numbers AWS account id - awsUID := conf.AccountID[0:3] + conf.AccountID[len(conf.AccountID)-2:] - conf.SopsAgeKeys = system.GetHomePath(system.RMKDir, system.SopsRootName, conf.Tenant+"-"+system.SopsRootName+"-"+awsUID) - conf.SopsBucketName = conf.Tenant + "-" + system.SopsRootName + "-" + awsUID - conf.Terraform.BucketName = conf.Tenant + "-" + system.TenantBucketName + "-" + awsUID - conf.Terraform.DDBTableName = system.TenantDDBTablePrefix + "-" + awsUID - - if err := conf.InitConfig(true).SetRootDomain(c, gitSpec.ID); err != nil { - return err - } - - if err := conf.CreateConfigFile(); err != nil { - return err - } - - if conf.ClusterProvider == system.AWSClusterProvider { - if conf.ClusterProvisionerSL { - // create dynamodb table for backend terraform - if err := conf.CreateDynamoDBTable(conf.Terraform.DDBTableName); err != nil { - return err - } - } - - // create s3 bucket for backend terraform - if err := conf.CreateBucket(conf.Terraform.BucketName); err != nil { - return err - } - - //create s3 bucket for sops age keys - if err := conf.CreateBucket(conf.SopsBucketName); err != nil { - return err - } - - if err := conf.DownloadFromBucket("", conf.SopsBucketName, conf.SopsAgeKeys, conf.Tenant); err != nil { - return err - } - } - - zap.S().Infof("time spent on initialization: %.fs", time.Since(start).Seconds()) - - return nil - } -} - -func configListAction(conf *config.Config, gitSpec *git_handler.GitSpec) cli.ActionFunc { - return func(c *cli.Context) error { - if err := system.ValidateNArg(c, 0); err != nil { - return err - } - - if err := gitSpec.GenerateID(); err != nil { - return err - } - - conf.Tenant = gitSpec.RepoPrefixName - return conf.GetConfigs(c.Bool("all")) - } -} - -func configViewAction(conf *config.Config) cli.ActionFunc { - return func(c *cli.Context) error { - if err := system.ValidateNArg(c, 0); err != nil { - return err - } - - if c.String("log-format") == "json" { - serializeJsonConfig, err := conf.SerializeJsonConfig() - if err != nil { - return err - } - - zap.L().Info("RMK", zap.Any("config", json.RawMessage(serializeJsonConfig))) - return nil - } - - serializeConfig, err := conf.SerializeConfig() - if err != nil { - return err - } - - zap.S().Infof("loaded config file by path: %s", c.String("config")) - fmt.Printf("%s\n", string(serializeConfig)) - - return nil - } -} diff --git a/commands/container_registry_category.go b/commands/container_registry_category.go deleted file mode 100644 index 7e93127..0000000 --- a/commands/container_registry_category.go +++ /dev/null @@ -1,91 +0,0 @@ -package commands - -import ( - "fmt" - "strings" - - "github.com/urfave/cli/v2" - "go.uber.org/zap" - - "rmk/config" - "rmk/system" -) - -type DockerRunner interface { - dockerLogin() error - dockerLogout() error -} - -type CRCommands struct { - *ReleaseCommands -} - -func newCRCommands(conf *config.Config, ctx *cli.Context, workDir string) *CRCommands { - return &CRCommands{&ReleaseCommands{Conf: conf, Ctx: ctx, WorkDir: workDir}} -} - -func (cr *CRCommands) docker(args ...string) *system.SpecCMD { - return &system.SpecCMD{ - Args: append([]string{}, args...), - Command: "docker", - Dir: cr.WorkDir, - Ctx: cr.Ctx.Context, - DisableStdOut: true, - Debug: false, - } -} - -func (cr *CRCommands) dockerLogin() error { - credentials, err := cr.Conf.AwsConfigure.GetECRCredentials(cr.Conf.AWSECRRegion) - if err != nil { - return err - } - - if token, ok := credentials[cr.Conf.AWSECRUserName]; !ok { - return fmt.Errorf("failed to get ECR token") - } else { - if cr.Ctx.Bool("get-token") { - fmt.Println(token) - return nil - } - - cr.SpecCMD = cr.docker("login", "--username", cr.Conf.AWSECRUserName, "--password", token, - cr.Conf.AWSECRHost) - if err := runner(cr).runCMD(); err != nil { - return err - } - - if !strings.Contains(cr.SpecCMD.StderrBuf.String(), "Using --password") { - return fmt.Errorf(strings.ReplaceAll(cr.SpecCMD.StderrBuf.String(), "\n", "")) - } - - zap.S().Info(strings.ReplaceAll(cr.SpecCMD.StdoutBuf.String(), "\n", "")) - } - - return nil -} - -func (cr *CRCommands) dockerLogout() error { - cr.SpecCMD = cr.docker("logout", cr.Conf.AWSECRHost) - if err := runner(cr).runCMD(); err != nil { - return err - } - - zap.S().Info(strings.ReplaceAll(cr.SpecCMD.StdoutBuf.String(), "\n", "")) - - return nil -} - -func containerRegistryAction(conf *config.Config, action func(dockerRunner DockerRunner) error) cli.ActionFunc { - return func(c *cli.Context) error { - if err := system.ValidateArtifactModeDefault(c, ""); err != nil { - return err - } - - if err := system.ValidateNArg(c, 0); err != nil { - return err - } - - return action(newCRCommands(conf, c, system.GetPwdPath(""))) - } -} diff --git a/commands/k3d_category.go b/commands/k3d_category.go deleted file mode 100644 index 10a67ee..0000000 --- a/commands/k3d_category.go +++ /dev/null @@ -1,179 +0,0 @@ -package commands - -import ( - "fmt" - "os" - "path/filepath" - "strings" - - "github.com/urfave/cli/v2" - - "rmk/config" - "rmk/system" -) - -type K3DRunner interface { - createDeleteK3DCluster() error - importImageToK3DCluster() error - listK3DClusters() error - startStopK3DCluster() error -} - -type K3DCommands struct { - *ReleaseCommands -} - -func newK3DCommands(conf *config.Config, ctx *cli.Context, workDir string) *K3DCommands { - return &K3DCommands{&ReleaseCommands{Conf: conf, Ctx: ctx, WorkDir: workDir}} -} - -func (k *K3DCommands) k3d(args ...string) *system.SpecCMD { - return &system.SpecCMD{ - Args: append([]string{}, args...), - Command: "k3d", - Dir: k.WorkDir, - Ctx: k.Ctx.Context, - DisableStdOut: false, - Debug: false, - } -} - -func (k *K3DCommands) prepareK3D(args ...string) error { - k.SpecCMD = k.k3d(args...) - k.SpecCMD.Debug = true - credentials, err := k.Conf.AwsConfigure.GetECRCredentials(k.Conf.AWSECRRegion) - if err != nil { - return err - } - - k.SpecCMD.Envs = append(k.SpecCMD.Envs, "K3D_NAME="+k.Conf.Name) - - if token, ok := credentials[k.Conf.AWSECRUserName]; !ok { - return fmt.Errorf("failed to get ECR token") - } else { - k.SpecCMD.Envs = append(k.SpecCMD.Envs, "K3D_AWS_ECR_USER="+k.Conf.AWSECRUserName, "K3D_AWS_ECR_PASSWORD="+token) - } - - if len(k.Ctx.String("k3d-volume-host-path")) > 0 { - k.SpecCMD.Envs = append(k.SpecCMD.Envs, "K3D_VOLUME_HOST_PATH="+k.Ctx.String("k3d-volume-host-path")) - return nil - } - - k.SpecCMD.Envs = append(k.SpecCMD.Envs, "K3D_VOLUME_HOST_PATH="+system.GetPwdPath("")) - - return nil -} - -func (k *K3DCommands) createDeleteK3DCluster() error { - var k3dDst string - - k.K3DCluster = true - if _, _, err := k.getKubeContext(); err != nil { - return err - } - - for name, pkg := range k.Conf.Clusters { - if strings.HasPrefix(name, system.K3DConfigPrefix) { - k3dDst = pkg.DstPath - break - } - } - - if len(k3dDst) == 0 { - return fmt.Errorf("cluster provider with name %s not found", system.K3DConfigPrefix) - } - - match, err := system.WalkMatch(k3dDst, system.K3DConfigPrefix+".yaml") - if err != nil { - return err - } - - if len(match) == 0 { - return fmt.Errorf("configuration file for %s not found", system.K3DConfigPrefix) - } - - if err := k.prepareK3D("cluster", k.Ctx.Command.Name, "--config", match[0]); err != nil { - return err - } - - // Creating specific dir for k3d registry configuration - k3dRegistryHostPath := filepath.Join(filepath.Dir(match[0]), system.K3DConfigPrefix) - k.SpecCMD.Envs = append(k.SpecCMD.Envs, "K3D_REGISTRY_HOST_PATH="+k3dRegistryHostPath) - - if err := os.RemoveAll(k3dRegistryHostPath); err != nil { - return err - } - - if err := os.MkdirAll(k3dRegistryHostPath, 0755); err != nil { - return err - } - - return runner(k).runCMD() -} - -func (k *K3DCommands) importImageToK3DCluster() error { - if err := k.prepareK3D(append(append([]string{}, "image", "import", "--cluster", k.Conf.Name, "--keep-tools"), - k.Ctx.StringSlice("k3d-import-image")...)...); err != nil { - return err - } - - return runner(k).runCMD() -} - -func (k *K3DCommands) listK3DClusters() error { - k.K3DCluster = true - if _, _, err := k.getKubeContext(); err != nil { - return err - } - - if err := k.prepareK3D("cluster", k.Ctx.Command.Name); err != nil { - return err - } - - return runner(k).runCMD() -} - -func (k *K3DCommands) startStopK3DCluster() error { - k.K3DCluster = true - if _, _, err := k.getKubeContext(); err != nil { - return err - } - - if err := k.prepareK3D("cluster", k.Ctx.Command.Name, k.Conf.Name); err != nil { - return err - } - - return runner(k).runCMD() -} - -func K3DCreateAction(conf *config.Config) cli.ActionFunc { - return func(c *cli.Context) error { - if err := system.ValidateArtifactModeDefault(c, ""); err != nil { - return err - } - - if err := system.ValidateNArg(c, 0); err != nil { - return err - } - - if err := resolveDependencies(conf.InitConfig(false), c, false); err != nil { - return err - } - - return newK3DCommands(conf, c, system.GetPwdPath("")).createDeleteK3DCluster() - } -} - -func K3DAction(conf *config.Config, action func(k3dRunner K3DRunner) error) cli.ActionFunc { - return func(c *cli.Context) error { - if err := system.ValidateArtifactModeDefault(c, ""); err != nil { - return err - } - - if err := system.ValidateNArg(c, 0); err != nil { - return err - } - - return action(newK3DCommands(conf, c, system.GetPwdPath(""))) - } -} diff --git a/commands/rmk_category.go b/commands/rmk_category.go deleted file mode 100644 index a1269cb..0000000 --- a/commands/rmk_category.go +++ /dev/null @@ -1,89 +0,0 @@ -package commands - -import ( - "fmt" - - "github.com/Masterminds/semver" - "github.com/urfave/cli/v2" - "go.uber.org/zap" - - "rmk/system" -) - -func completionAction() cli.ActionFunc { - return func(c *cli.Context) error { - if err := system.ValidateNArg(c, 0); err != nil { - return err - } - - fmt.Println(system.CompletionZshScript) - - return nil - } -} - -func docGenerateAction() cli.ActionFunc { - return func(c *cli.Context) error { - if err := system.ValidateNArg(c, 0); err != nil { - return err - } - - man, err := c.App.ToMarkdown() - if err != nil { - return nil - } - - fmt.Println(man) - - return nil - } -} - -func updateAction() cli.ActionFunc { - return func(c *cli.Context) error { - if err := system.ValidateNArg(c, 0); err != nil { - return err - } - - var version string - latestPath := "latest" - - if c.Bool("release-candidate") { - latestPath = "latest-rc" - } - - metadata, err := getRMKArtifactMetadata(latestPath) - if err != nil { - return err - } - - if len(c.String("version")) > 0 { - v, err := semver.NewVersion(c.String("version")) - if err != nil { - return err - } - - version = v.Original() - } - - verCurrent, _ := semver.NewVersion(fmt.Sprintf("%v", c.App.Metadata["version"])) - verFound, _ := semver.NewVersion(metadata.Version) - binaryName := fmt.Sprintf("%s", c.App.Metadata["binaryName"]) - if verCurrent.LessThan(verFound) && len(version) == 0 { - zap.S().Infof("newer release version RMK available: %s", verFound.Original()) - if err := updateRMK(binaryName, latestPath, false, true, c); err != nil { - return err - } - } else if len(version) > 0 { - zap.S().Infof("update current RMK version from %s to %s", - c.App.Metadata["version"], version) - if err := updateRMK(binaryName, version, false, true, c); err != nil { - return err - } - } else { - zap.S().Infof("installed RMK version %s is up-to-date", verCurrent.Original()) - } - - return nil - } -} diff --git a/config/config.go b/config/config.go index b6b5a1b..0f05425 100644 --- a/config/config.go +++ b/config/config.go @@ -6,9 +6,8 @@ import ( "fmt" "os" "path/filepath" - "reflect" + "regexp" "runtime" - "strconv" "strings" "text/template" @@ -17,43 +16,33 @@ import ( "go.uber.org/zap" "gopkg.in/yaml.v3" - "rmk/aws_provider" - "rmk/system" + "rmk/git_handler" + "rmk/providers/aws_provider" + "rmk/providers/azure_provider" + "rmk/providers/google_provider" + "rmk/util" ) type Config struct { - Name string `yaml:"name,omitempty"` - Tenant string `yaml:"tenant,omitempty"` - Environment string `yaml:"environment,omitempty"` - ConfigFrom string `yaml:"config-from,omitempty"` - ArtifactMode string `yaml:"artifact-mode,omitempty"` - RootDomain string `yaml:"root-domain,omitempty"` - CloudflareToken string `yaml:"cloudflare-token,omitempty"` - GitHubToken string `yaml:"github-token,omitempty"` - S3ChartsRepoRegion string `yaml:"s3-charts-repo-region"` - ClusterProvider string `yaml:"cluster-provider"` - SlackNotifications bool `yaml:"slack-notifications"` - SlackWebHook string `yaml:"slack-webhook,omitempty"` - SlackChannel string `yaml:"slack-channel,omitempty"` - SlackMsgDetails []string `yaml:"slack-message-details,omitempty"` - SopsAgeKeys string `yaml:"sops-age-keys,omitempty"` - SopsBucketName string `yaml:"sops-bucket-name,omitempty"` - AWSECRHost string `yaml:"aws-ecr-host,omitempty"` - AWSECRRegion string `yaml:"aws-ecr-region,omitempty"` - AWSECRUserName string `yaml:"aws-ecr-user-name,omitempty"` - AWSMFAProfile string `yaml:"aws-mfa-profile,omitempty"` - AWSMFATokenExpiration string `yaml:"aws-mfa-token-expiration,omitempty"` - *aws_provider.AwsConfigure `yaml:"aws,omitempty"` - Terraform `yaml:"terraform,omitempty"` - ClusterProvisionerSL bool `yaml:"cluster-provisioner-state-locking"` - ExportedVars `yaml:"exported-vars,omitempty"` - ProgressBar bool `yaml:"progress-bar"` - ProjectFile `yaml:"project-file"` -} - -type ExportedVars struct { - TerraformOutput map[string]interface{} `yaml:"terraform-output,omitempty"` - Env map[string]string `yaml:"env,omitempty"` + Name string `yaml:"name,omitempty"` + Tenant string `yaml:"tenant,omitempty"` + Environment string `yaml:"environment,omitempty"` + RootDomain string `yaml:"root-domain,omitempty"` + GitHubToken string `yaml:"github-token,omitempty"` + ClusterProvider string `yaml:"cluster-provider"` + SlackNotifications bool `yaml:"slack-notifications"` + SlackWebHook string `yaml:"slack-webhook,omitempty"` + SlackChannel string `yaml:"slack-channel,omitempty"` + SlackMsgDetails []string `yaml:"slack-message-details,omitempty"` + SopsAgeKeys string `yaml:"sops-age-keys,omitempty"` + AWSMFAProfile string `yaml:"aws-mfa-profile,omitempty"` + AWSMFATokenExpiration string `yaml:"aws-mfa-token-expiration,omitempty"` + GCPRegion string `yaml:"gcp-region,omitempty"` + *aws_provider.AwsConfigure `yaml:"aws,omitempty"` + *azure_provider.AzureConfigure `yaml:"azure,omitempty"` + *google_provider.GCPConfigure `yaml:"gcp,omitempty"` + ProgressBar bool `yaml:"progress-bar"` + ProjectFile `yaml:"project-file"` } type HookMapping struct { @@ -69,7 +58,6 @@ type Package struct { Url string `yaml:"url,omitempty"` Checksum string `yaml:"checksum,omitempty"` Artifacts []string `yaml:"-"` - ArtifactUrl string `yaml:"artifact-url,omitempty"` HelmfileTenant string `yaml:"-"` OsLinux string `yaml:"os-linux,omitempty"` OsMac string `yaml:"os-mac,omitempty"` @@ -80,7 +68,6 @@ type Package struct { } type Inventory struct { - Clusters map[string]*Package `yaml:"clusters,omitempty"` HelmPlugins map[string]*Package `yaml:"helm-plugins,omitempty"` Hooks map[string]*Package `yaml:"hooks,omitempty"` Tools map[string]*Package `yaml:"tools,omitempty"` @@ -90,9 +77,9 @@ type Project struct { Dependencies []Package `yaml:"dependencies,omitempty"` HooksMapping []HookMapping `yaml:"hooks-mapping,omitempty"` Spec struct { - Environments []string `yaml:"environments,omitempty"` - Owners []string `yaml:"owners,omitempty"` - Scopes []string `yaml:"scopes,omitempty"` + Environments map[string]*ProjectRootDomain `yaml:"environments,omitempty"` + Owners []string `yaml:"owners,omitempty"` + Scopes []string `yaml:"scopes,omitempty"` } `yaml:"spec,omitempty"` } @@ -101,28 +88,13 @@ type ProjectFile struct { Inventory `yaml:"inventory,omitempty"` } -type Terraform struct { - BucketName string `yaml:"bucket-name,omitempty"` - BucketKey string `yaml:"bucket-key,omitempty"` - DDBTableName string `yaml:"dynamodb-table-name,omitempty"` +type ProjectRootDomain struct { + RootDomain string `yaml:"root-domain,omitempty"` } -func (conf *Config) InitConfig(terraformOutput bool) *Config { +func (conf *Config) InitConfig() *Config { conf.ProjectFile = ProjectFile{} - if err := conf.ReadProjectFile(system.GetPwdPath(system.TenantProjectFile)); err != nil { - zap.S().Fatal(err) - } - - if !terraformOutput { - return conf - } - - conf.ExportedVars = ExportedVars{ - TerraformOutput: make(map[string]interface{}), - Env: make(map[string]string), - } - - if err := conf.GetTerraformOutputs(); err != nil { + if err := conf.ReadProjectFile(util.GetPwdPath(util.TenantProjectFile)); err != nil { zap.S().Fatal(err) } @@ -142,102 +114,67 @@ func (conf *Config) SerializeJsonConfig() ([]byte, error) { } func (conf *Config) GetConfigs(all bool) error { - var tenantPattern string - configsPath := system.GetHomePath(system.RMKDir, system.RMKConfig) + var ( + patternTenant string + patternTaskNum *regexp.Regexp + patternSemVer *regexp.Regexp + patternBranch *regexp.Regexp + ) + + configsPath := util.GetHomePath(util.RMKDir, util.RMKConfig) if all { - tenantPattern = "" + patternTenant = "" } else { - tenantPattern = conf.Tenant + patternTenant = conf.Tenant + + patternBranch = regexp.MustCompile(`^` + patternTenant + + `-(` + git_handler.DefaultDevelop + `|` + git_handler.DefaultStaging + `|` + git_handler.DefaultProduction + `)$`) + patternSemVer = regexp.MustCompile(`^` + patternTenant + `-v\d+-\d+-\d+(-[a-z]+)?$`) + patternTaskNum = regexp.MustCompile(`^` + patternTenant + `-[a-z]+-\d+$`) } - match, err := system.WalkMatch(configsPath, tenantPattern+"*.yaml") + match, err := util.WalkMatch(configsPath, patternTenant+"*.yaml") if err != nil { return err } for _, val := range match { - fmt.Printf("- %s\n", strings.TrimSuffix(filepath.Base(val), filepath.Ext(filepath.Base(val)))) - } + rmkConfig := strings.TrimSuffix(filepath.Base(val), filepath.Ext(filepath.Base(val))) - return nil -} - -func (conf *Config) SetRootDomain(c *cli.Context, gitSpecID string) error { - hostedZoneVar := system.TerraformVarsPrefix + system.TerraformVarHostedZoneName - if !c.IsSet("root-domain") { - if hostedZoneName, ok := conf.TerraformOutput[hostedZoneVar]; ok && len(hostedZoneName.(string)) > 0 { - if err := c.Set("root-domain", hostedZoneName.(string)); err != nil { - return err - } + if all { + fmt.Printf("- %s\n", rmkConfig) } else { - if err := c.Set("root-domain", gitSpecID+system.TenantDomainSuffix); err != nil { - return err + switch { + case patternBranch.MatchString(rmkConfig): + fmt.Printf("- %s\n", rmkConfig) + case patternSemVer.MatchString(rmkConfig): + fmt.Printf("- %s\n", rmkConfig) + case patternTaskNum.MatchString(rmkConfig): + fmt.Printf("- %s\n", rmkConfig) } } } - conf.RootDomain = c.String("root-domain") - return nil } -func (conf *Config) GetTerraformOutputs() error { - type GetVar struct { - Type interface{} - Value interface{} - } - - var ( - raw map[string]*json.RawMessage - outputs map[string]*json.RawMessage - getVar *GetVar - ) - - checkWorkspace, err := conf.BucketKeyExists("", conf.Terraform.BucketName, "env:/"+conf.Name+"/tf.tfstate") - if err != nil { - return err - } - - if !checkWorkspace { - return nil - } - - data, err := conf.GetFileData(conf.Terraform.BucketName, "env:/"+conf.Name+"/tf.tfstate") - if err != nil { - return err - } - - if err := json.Unmarshal(data, &raw); err != nil { - return err - } - - if err := json.Unmarshal(*raw["outputs"], &outputs); err != nil { - return err - } - - if len(outputs) == 0 { - return nil +func (conf *Config) SetRootDomain(c *cli.Context, gitSpecID string) error { + for env, val := range conf.Spec.Environments { + check := strings.Split(val.RootDomain, "*.") + if len(check) > 2 { + return fmt.Errorf("root-domain not set correctly for environment %s", env) + } } - for key := range outputs { - if strings.Contains(key, system.TerraformVarsPrefix) { - if err := json.Unmarshal(*outputs[key], &getVar); err != nil { - return err - } - - envKey := strings.ToUpper(strings.ReplaceAll(key, system.TerraformVarsPrefix, "")) - - switch { - case reflect.TypeOf(getVar.Value).Kind() == reflect.String && getVar.Type == reflect.String.String(): - conf.TerraformOutput[key] = getVar.Value - conf.Env[envKey] = getVar.Value.(string) - case reflect.TypeOf(getVar.Value).Kind() == reflect.Bool && getVar.Type == reflect.Bool.String(): - conf.TerraformOutput[key] = getVar.Value - conf.Env[envKey] = strconv.FormatBool(getVar.Value.(bool)) - default: - zap.S().Warnf("Terraform output variable %s will not be exported as environment variable, "+ - "does not match string or boolean types, current type: %s", key, getVar.Type) + for env, val := range conf.Spec.Environments { + if env == conf.Environment { + if regexp.MustCompile(`^\*\.`).MatchString(val.RootDomain) { + conf.RootDomain = strings.ReplaceAll(val.RootDomain, "*", gitSpecID) + } else if len(val.RootDomain) > 0 { + conf.RootDomain = val.RootDomain + } else { + conf.RootDomain = "localhost" } } } @@ -282,27 +219,6 @@ func (pf *ProjectFile) parseProjectFileData() error { if err != nil { return err } - - if len(strings.Split(pf.Dependencies[key].Name, ".")) > 0 { - pf.Dependencies[key].HelmfileTenant = strings.Split(pf.Dependencies[key].Name, ".")[0] - } - - pf.Dependencies[key].ArtifactUrl, err = pf.ParseTemplate(template.New("Dependencies"), pf.Dependencies[key], dep.ArtifactUrl) - if err != nil { - return err - } - } - - for key, provider := range pf.Clusters { - if _, err := semver.NewVersion(provider.Version); err != nil { - return fmt.Errorf("%s %s for section inventory.clusters", strings.ToLower(err.Error()), provider.Version) - } - - pf.Clusters[key].Name = key - pf.Clusters[key].Url, err = pf.ParseTemplate(template.New("Clusters"), pf.Clusters[key], provider.Url) - if err != nil { - return err - } } for key, plugin := range pf.HelmPlugins { @@ -370,7 +286,7 @@ func (conf *Config) ReadConfigFile(path string) error { } func (conf *Config) CreateConfigFile() error { - if err := os.MkdirAll(system.GetHomePath(system.RMKDir, system.RMKConfig), 0755); err != nil { + if err := os.MkdirAll(util.GetHomePath(util.RMKDir, util.RMKConfig), 0755); err != nil { return err } @@ -379,5 +295,5 @@ func (conf *Config) CreateConfigFile() error { return err } - return os.WriteFile(system.GetHomePath(system.RMKDir, system.RMKConfig, conf.Name+".yaml"), data, 0644) + return os.WriteFile(util.GetHomePath(util.RMKDir, util.RMKConfig, conf.Name+".yaml"), data, 0644) } diff --git a/docs/commands.md b/docs/commands.md index 2e29222..cd90314 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -18,8 +18,8 @@ rmk Command line tool for reduced management of the provision of Kubernetes clusters in different environments and management of service releases. **BuiltBy:** goreleaser
-**Commit:** 7a0f199
-**Date:** 2024-09-04T07:46:16Z
+**Commit:** 6e0a217
+**Date:** 2024-12-19T09:18:59Z
**Target:** linux_amd64 **Usage**: @@ -45,27 +45,35 @@ rmk [GLOBAL OPTIONS] command [COMMAND OPTIONS] [ARGUMENTS...] Cluster management -#### container-registry, c +#### capi, c -Container registry management +CAPI cluster management -##### login +##### create, c -Log in to container registry +Create CAPI management cluster -**--get-token, -g**: get ECR token for authentication +**--k3d-volume-host-path, --kv**="": host local directory path for mount into K3D cluster (default: present working directory) -##### logout +##### delete, d -Log out from container registry +Delete CAPI management cluster -#### destroy, d +##### destroy -Destroy AWS cluster using Terraform +Destroy K8S target (workload) cluster -#### list, l +##### list, l + +List CAPI management clusters + +##### provision, p -List all Terraform available workspaces +Provision K8S target (workload) cluster + +##### update, u + +Update CAPI management cluster #### k3d, k @@ -99,30 +107,6 @@ Start K3D cluster Stop K3D cluster -#### provision, p - -Provision AWS cluster using Terraform - -**--plan, -p**: creates an execution Terraform plan - -#### state, t - -State cluster management using Terraform - -##### delete, d - -Delete resource from Terraform state - -**--resource-address, --ra**="": resource address for delete from Terraform state - -##### list, l - -List resources from Terraform state - -##### refresh, r - -Update state file for AWS cluster using Terraform - #### switch, s Switch Kubernetes context for tenant cluster @@ -145,37 +129,39 @@ Configuration management Initialize configuration for current tenant and selected environment -**--artifact-mode, --am**="": choice of artifact usage model, available: none, online (default: "none") +**--aws-access-key-id, --awid**="": AWS access key ID for IAM user -**--aws-ecr-host, --aeh**="": AWS ECR host (default: "288509344804.dkr.ecr.eu-north-1.amazonaws.com") +**--aws-region, --awr**="": AWS region for current AWS account -**--aws-ecr-region, --aer**="": AWS region for specific ECR host (default: "eu-north-1") +**--aws-secret-access-key, --awsk**="": AWS secret access key for IAM user -**--aws-ecr-user-name, --aeun**="": AWS ECR user name (default: "AWS") +**--aws-session-token, --awst**="": AWS session token for IAM user -**--aws-reconfigure, -r**: force AWS profile creation +**--azure-client-id, --azid**="": Azure client ID for Service Principal -**--aws-reconfigure-artifact-license, -l**: force AWS profile creation for artifact license, used only if RMK config option artifact-mode has values: online, offline +**--azure-client-secret, --azp**="": Azure client secret for Service Principal -**--cloudflare-token, --cft**="": Cloudflare API token for provision NS records +**--azure-location, --azl**="": Azure location -**--cluster-provider, --cp**="": select cluster provider to provision clusters (default: "aws") +**--azure-service-principle, --azsp**: Azure service principal STDIN content -**--cluster-provisioner-state-locking, -c**: disable or enable cluster provisioner state locking +**--azure-subscription-id, --azs**="": Azure subscription ID for current platform domain -**--config-from-environment, --cfe**="": inheritance of RMK config credentials from environments: develop, staging, production +**--azure-tenant-id, --azt**="": Azure tenant ID for Service Principal -**--github-token, --ght**="": personal access token for download GitHub artifacts +**--cluster-provider, --cp**="": cluster provider for provisioning (default: "k3d") -**--progress-bar, -p**: globally disable or enable progress bar for download process +**--gcp-region, --gr**="": GCP region + +**--github-token, --ght**="": GitHub personal access token, required when using private repositories -**--root-domain, --rd**="": domain name for external access to app services via ingress controller +**--google-application-credentials, --gac**="": path to GCP service account credentials JSON file -**--s3-charts-repo-region, --scrr**="": location constraint region of S3 charts repo (default: "eu-north-1") +**--progress-bar, -p**: globally disable or enable progress bar for download process -**--slack-channel, --sc**="": channel name for Slack notification +**--slack-channel, --sc**="": channel name for Slack notifications -**--slack-message-details, --smd**="": additional information for body of Slack message +**--slack-message-details, --smd**="": additional information for body of Slack messages **--slack-notifications, -n**: enable Slack notifications @@ -225,6 +211,12 @@ Generate project directories and files structure **--create-sops-age-keys, -c**: create SOPS age keys for generated project structure +**--environments, -e**="": list project environments. Root domain can take form of .root-domain= + +**--owners, -o**="": list project owners + +**--scopes, -s**="": list project scopes + #### update, u Update project file with specific dependencies version @@ -261,8 +253,6 @@ Destroy releases **--helmfile-log-level, --hll**="": Helmfile log level severity, available: debug, info, warn, error (default: "error") -**--output, -o**="": output format, available: short, yaml (default: "short") - **--selector, -l**="": only run using releases that match labels. Labels can take form of foo=bar or foo!=bar **--skip-context-switch, -s**: skip context switch for not provisioned cluster diff --git a/docs/configuration/configuration-management.md b/docs/configuration/configuration-management.md index 70c2a98..a3e4e9e 100644 --- a/docs/configuration/configuration-management.md +++ b/docs/configuration/configuration-management.md @@ -104,45 +104,3 @@ rmk config delete ``` > When deleting the current RMK configuration, the respective AWS profile files will be deleted as well. - -## Use upstream artifact for the downstream project's repository - -RMK supports downloading an upstream project's artifact using additional "license" AWS credentials. -To switch RMK to the artifact usage mode, you need to use additional flags when initializing the RMK configuration -for the current project. Additionally, before starting the initialization, you need to install the required version -of the upstream project to which you want to update. -For example: - -```yaml -project: - dependencies: - - name: deps.bootstrap.infra - version: v2.17.0 - url: git::https://github.com/edenlabllc/{{.Name}}.git?ref={{.Version}} - artifact-url: https://edenlabllc-{{.HelmfileTenant}}-artifacts-infra.s3.eu-north-1.amazonaws.com/{{.Version}}/{{.HelmfileTenant}}-{{.Version}}.tar.gz - # ... -``` - -> The `artifact-url` field is required and contains the artifact URL generation template which consists -> of the following [fields](project-management/preparation-of-project-repository.md#projectyaml). - -Set the `version` field to the version of the upstream project for the current project. For example: - -```shell -# artifact usage modes: none|online (default: "none") -rmk config init --artifact-mode=online -``` - -> Currently, only two artifact modes are supported: -> -> - `none`: The standard mode of RMK which is used for development normally, the codebase will be downloaded from GitHub repositories. -> The mode does not require the presence of the special "license" credentials. -> - `online`: Switches RMK to work with artifacts. In this mode, RMK will not use any credentials for GitHub -> (e.g., personal access tokens), but will request additional license AWS credentials to download and unpack -> the artifact from a repository like AWS S3. - -To change the "license" AWS credentials when in the online artifact mode, use the following command: - -```shell -rmk config init --aws-reconfigure-artifact-license -``` diff --git a/docs/configuration/project-management/preparation-of-project-repository.md b/docs/configuration/project-management/preparation-of-project-repository.md index ed9e42b..f1529ed 100644 --- a/docs/configuration/project-management/preparation-of-project-repository.md +++ b/docs/configuration/project-management/preparation-of-project-repository.md @@ -38,10 +38,7 @@ and contains the following main sections: # Required, dependencies upstream project's repository version in `SemVer2` format, also can be a branch name or a commit hash. version: # Required, dependencies upstream project's repository URL. - url: git::https://github.com//{{.Name}}.git?ref={{.Version}} - # Optional, if it is expected that the dependency will be downloaded as an artifact from another storage. - artifact-url: https://-{{.HelmfileTenant}}-artifacts-infra.s3..amazonaws.com/{{.Version}}/{{.HelmfileTenant}}-{{.Version}}.tar.gz - + url: git::https://github.com//{{.Name}}.git?ref={{.Version}} # Optional, needed if you want automatic generation of the project structure from scratch. spec: # Required, list of available environments of the project (Git branches). @@ -115,7 +112,6 @@ project: - name: deps.bootstrap.infra version: v2.17.0 url: git::https://github.com/edenlabllc/{{.Name}}.git?ref={{.Version}} - artifact-url: https://edenlabllc-{{.HelmfileTenant}}-artifacts-infra.s3.eu-north-1.amazonaws.com/{{.Version}}/{{.HelmfileTenant}}-{{.Version}}.tar.gz spec: environments: - develop diff --git a/docs/release-notes.md b/docs/release-notes.md index 7feef1c..95ef96e 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1 +1,19 @@ -- #27 - Fixed development-and-release.md guides, links to them from README.md and index.md. +- #29 - Added support for AWS Secrets Manager to store SOPS Age keys. +- #29 - Added support for GCP Secrets Manager to store SOPS Age keys. +- #29 - Added support for Azure Key Vault to store SOPS Age keys. +- #29 - Added support for the AWS provider in Cluster API. +- #29 - Added support for the GCP provider in Cluster API. +- #29 - Added support for the Azure provider in Cluster API. +- #29 - Added the ability to create an SSH key pair for the AWS provider. +- #29 - Added GCP NAT router creation for the GCP provider. +- #29 - Added commands for managing Cluster API clusters to the cluster category commands. +- #29 - Added new configuration arguments for project generation. +- #29 - Fixed the output of the config list command for adjacent tenant names. +- #29 - Removed the artifact-mode flag and the functionality. +- #29 - Reworked the behaviour of the config init command to align with each cluster provider. +- #29 - Reworked the approach to obtaining the Kubernetes context for all cluster providers. +- #29 - Refactored the environment variables provisioning for all cluster providers in release category commands. +- #29 - Refactored the k3d list command. +- #29 - Restructured code files for improved organization. +- #29 - Changed root-domain configuration to be managed via the project.yaml file. +- #29 - Deprecated Terraform commands under the cluster category. diff --git a/git_handler/git.go b/git_handler/git.go index d318a9c..7379941 100644 --- a/git_handler/git.go +++ b/git_handler/git.go @@ -16,7 +16,7 @@ import ( "github.com/go-git/go-git/v5/plumbing/transport/ssh" "go.uber.org/zap" - "rmk/system" + "rmk/util" ) const ( @@ -108,7 +108,7 @@ func (g *GitSpec) GetBranchName() error { DetectDotGit: true, } - repo, err := git.PlainOpenWithOptions(system.GetPwdPath(""), &openOptions) + repo, err := git.PlainOpenWithOptions(util.GetPwdPath(""), &openOptions) if err != nil { return err } @@ -130,7 +130,7 @@ func (g *GitSpec) GetRepoPrefix() error { DetectDotGit: true, } - repo, err := git.PlainOpenWithOptions(system.GetPwdPath(""), &openOptions) + repo, err := git.PlainOpenWithOptions(util.GetPwdPath(""), &openOptions) if err != nil { return err } @@ -174,11 +174,11 @@ func (g *GitSpec) GenerateID() error { func (g *GitSpec) GitCommitPush(pathRF, msg, token string) error { var err error - if g.repo, err = git.PlainOpen(system.GetPwdPath("")); err != nil { + if g.repo, err = git.PlainOpen(util.GetPwdPath("")); err != nil { return err } - if pathRF, err = filepath.Rel(system.GetPwdPath(""), pathRF); err != nil { + if pathRF, err = filepath.Rel(util.GetPwdPath(""), pathRF); err != nil { return err } @@ -209,19 +209,19 @@ func (g *GitSpec) GitCommitPush(pathRF, msg, token string) error { return err } - reset := &system.SpecCMD{ + reset := &util.SpecCMD{ Args: []string{"reset", "--hard", "origin/" + g.headRef.Name().Short()}, Command: "git", - Dir: system.GetPwdPath(""), + Dir: util.GetPwdPath(""), Ctx: context.TODO(), DisableStdOut: true, Debug: false, } - cherryPick := &system.SpecCMD{ + cherryPick := &util.SpecCMD{ Args: []string{"cherry-pick", hash.String()}, Command: "git", - Dir: system.GetPwdPath(""), + Dir: util.GetPwdPath(""), Ctx: context.TODO(), DisableStdOut: true, Debug: false, @@ -300,7 +300,7 @@ func (g *GitSpec) getAuthMethod(token string) (transport.AuthMethod, error) { return &http.BasicAuth{Username: "git", Password: token}, nil } - return ssh.NewPublicKeysFromFile("git", system.GetHomePath(system.GitSSHPrivateKey), "") + return ssh.NewPublicKeysFromFile("git", util.GetHomePath(util.GitSSHPrivateKey), "") } return nil, fmt.Errorf("failed to detect auth method") diff --git a/go.mod b/go.mod index d566e8b..aea77d4 100644 --- a/go.mod +++ b/go.mod @@ -1,113 +1,171 @@ module rmk -go 1.21 +go 1.22.0 + +toolchain go1.22.8 require ( + cloud.google.com/go/auth v0.10.2 + cloud.google.com/go/secretmanager v1.11.5 + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0 + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 + github.com/Azure/azure-sdk-for-go/sdk/keyvault/azsecrets v0.12.0 + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v6 v6.1.0 + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault v1.4.0 + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 github.com/Masterminds/semver v1.5.0 github.com/Masterminds/sprig/v3 v3.2.3 - github.com/aws/aws-sdk-go-v2 v1.24.1 + github.com/aws/aws-sdk-go-v2 v1.32.6 github.com/aws/aws-sdk-go-v2/config v1.26.3 github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.15.11 - github.com/aws/aws-sdk-go-v2/service/dynamodb v1.26.8 - github.com/aws/aws-sdk-go-v2/service/ecr v1.24.7 + github.com/aws/aws-sdk-go-v2/service/ec2 v1.187.0 + github.com/aws/aws-sdk-go-v2/service/eks v1.51.1 github.com/aws/aws-sdk-go-v2/service/iam v1.28.7 github.com/aws/aws-sdk-go-v2/service/s3 v1.48.0 + github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.34.7 github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 - github.com/aws/smithy-go v1.19.0 + github.com/aws/smithy-go v1.22.1 github.com/cheggaaa/pb v1.0.29 + github.com/ghodss/yaml v1.0.0 github.com/go-git/go-git/v5 v5.11.0 + github.com/google/go-cmp v0.6.0 github.com/google/go-github v17.0.0+incompatible - github.com/hashicorp/go-getter v1.7.3 + github.com/googleapis/gax-go/v2 v2.12.3 + github.com/hashicorp/go-getter v1.7.5 github.com/slack-go/slack v0.12.3 github.com/urfave/cli/v2 v2.27.1 - go.uber.org/zap v1.26.0 - golang.org/x/crypto v0.18.0 - golang.org/x/oauth2 v0.16.0 + go.uber.org/zap v1.27.0 + golang.org/x/crypto v0.27.0 + golang.org/x/net v0.29.0 + golang.org/x/oauth2 v0.22.0 + google.golang.org/api v0.177.0 gopkg.in/yaml.v3 v3.0.1 + k8s.io/api v0.31.1 + k8s.io/apimachinery v0.31.1 + k8s.io/client-go v0.31.1 mvdan.cc/sh/v3 v3.8.0 ) require ( - cloud.google.com/go v0.110.2 // indirect - cloud.google.com/go/compute v1.20.1 // indirect - cloud.google.com/go/compute/metadata v0.2.3 // indirect - cloud.google.com/go/iam v0.13.0 // indirect - cloud.google.com/go/storage v1.29.0 // indirect + cloud.google.com/go v0.112.2 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect + cloud.google.com/go/compute/metadata v0.5.1 // indirect + cloud.google.com/go/iam v1.1.6 // indirect + cloud.google.com/go/storage v1.39.1 // indirect dario.cat/mergo v1.0.0 // indirect - github.com/BurntSushi/toml v1.3.2 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect + github.com/BurntSushi/toml v1.4.0 // indirect github.com/Masterminds/goutils v1.1.1 // indirect - github.com/Masterminds/semver/v3 v3.2.0 // indirect + github.com/Masterminds/semver/v3 v3.2.1 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect github.com/aws/aws-sdk-go v1.44.122 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.16.14 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.25 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.25 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 // indirect github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.10 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 // indirect github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.10 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.8.11 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.3 // indirect github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.10 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.18.6 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.6 // indirect github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect - github.com/cloudflare/circl v1.3.3 // indirect + github.com/cloudflare/circl v1.3.7 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/cyphar/filepath-securejoin v0.2.4 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emicklei/go-restful/v3 v3.12.1 // indirect github.com/emirpasic/gods v1.18.1 // indirect + github.com/fatih/color v1.17.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.5.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v5 v5.2.1 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/golang/protobuf v1.5.3 // indirect - github.com/google/go-cmp v0.6.0 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-querystring v1.1.0 // indirect - github.com/google/s2a-go v0.1.4 // indirect - github.com/google/uuid v1.3.0 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect - github.com/googleapis/gax-go/v2 v2.11.0 // indirect - github.com/gorilla/websocket v1.4.2 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/s2a-go v0.1.8 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect + github.com/gorilla/websocket v1.5.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-safetemp v1.0.0 // indirect github.com/hashicorp/go-version v1.6.0 // indirect - github.com/huandu/xstrings v1.3.3 // indirect - github.com/imdario/mergo v0.3.11 // indirect + github.com/huandu/xstrings v1.4.0 // indirect + github.com/imdario/mergo v0.3.16 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/klauspost/compress v1.15.11 // indirect - github.com/mattn/go-runewidth v0.0.4 // indirect - github.com/mitchellh/copystructure v1.0.0 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect - github.com/mitchellh/reflectwalk v1.0.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/onsi/ginkgo/v2 v2.20.2 // indirect + github.com/onsi/gomega v1.34.2 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/rivo/uniseg v0.4.2 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sergi/go-diff v1.1.0 // indirect - github.com/shopspring/decimal v1.2.0 // indirect + github.com/shopspring/decimal v1.3.1 // indirect github.com/skeema/knownhosts v1.2.1 // indirect - github.com/spf13/cast v1.3.1 // indirect + github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect github.com/ulikunitz/xz v0.5.10 // indirect + github.com/x448/float16 v0.8.4 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect go.opencensus.io v0.24.0 // indirect - go.uber.org/multierr v1.10.0 // indirect - golang.org/x/mod v0.14.0 // indirect - golang.org/x/net v0.20.0 // indirect - golang.org/x/sys v0.17.0 // indirect - golang.org/x/term v0.17.0 // indirect - golang.org/x/text v0.14.0 // indirect - golang.org/x/tools v0.17.0 // indirect - golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect - google.golang.org/api v0.126.0 // indirect - google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc // indirect - google.golang.org/grpc v1.55.0 // indirect - google.golang.org/protobuf v1.31.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect + go.opentelemetry.io/otel v1.29.0 // indirect + go.opentelemetry.io/otel/metric v1.29.0 // indirect + go.opentelemetry.io/otel/trace v1.29.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/mod v0.21.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.25.0 // indirect + golang.org/x/term v0.24.0 // indirect + golang.org/x/text v0.18.0 // indirect + golang.org/x/time v0.5.0 // indirect + golang.org/x/tools v0.24.0 // indirect + google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240610135401-a8a62080eff3 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect + google.golang.org/grpc v1.66.2 // indirect + google.golang.org/protobuf v1.34.2 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20240521193020-835d969ad83a // indirect + k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/go.sum b/go.sum index 6474851..442110b 100644 --- a/go.sum +++ b/go.sum @@ -30,8 +30,8 @@ cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w9 cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU= cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA= -cloud.google.com/go v0.110.2 h1:sdFPBr6xG9/wkBbfhmUz/JmZC7X6LavQgcrVINrKiVA= -cloud.google.com/go v0.110.2/go.mod h1:k04UEeEtb6ZBRTv3dZz4CeJC3jKGxyhl0sAiVVquxiw= +cloud.google.com/go v0.112.2 h1:ZaGT6LiG7dBzi6zNOvVZwacaXlmf3lRqnC4DQzqyRQw= +cloud.google.com/go v0.112.2/go.mod h1:iEqjp//KquGIJV/m+Pk3xecgKNhV+ry+vVTsy4TbDms= cloud.google.com/go/aiplatform v1.22.0/go.mod h1:ig5Nct50bZlzV6NvKaTwmplLLddFx0YReh9WfTO5jKw= cloud.google.com/go/aiplatform v1.24.0/go.mod h1:67UUvRBKG6GTayHKV8DBv2RtR1t93YRu5B1P3x99mYY= cloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI= @@ -46,6 +46,10 @@ cloud.google.com/go/asset v1.8.0/go.mod h1:mUNGKhiqIdbr8X7KNayoYvyc4HbbFO9URsjby cloud.google.com/go/assuredworkloads v1.5.0/go.mod h1:n8HOZ6pff6re5KYfBXcFvSViQjDwxFkAkmUFffJRbbY= cloud.google.com/go/assuredworkloads v1.6.0/go.mod h1:yo2YOk37Yc89Rsd5QMVECvjaMKymF9OP+QXWlKXUkXw= cloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVoYoxeLBoj4XkKYscNI= +cloud.google.com/go/auth v0.10.2 h1:oKF7rgBfSHdp/kuhXtqU/tNDr0mZqhYbEh+6SiqzkKo= +cloud.google.com/go/auth v0.10.2/go.mod h1:xxA5AqpDrvS+Gkmo9RqrGGRh6WSNKKOXhY3zNOr38tI= +cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4= +cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q= cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0= cloud.google.com/go/automl v1.6.0/go.mod h1:ugf8a6Fx+zP0D59WLhqgTDsQI9w07o64uf/Is3Nh5p8= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= @@ -68,10 +72,8 @@ cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= cloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU= -cloud.google.com/go/compute v1.20.1 h1:6aKEtlUiwEpJzM001l0yFkpXmUVXaN8W+fbkb2AZNbg= -cloud.google.com/go/compute v1.20.1/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM= -cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= -cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +cloud.google.com/go/compute/metadata v0.5.1 h1:NM6oZeZNlYjiwYje+sYFjEpP0Q0zCan1bmQW/KmIrGs= +cloud.google.com/go/compute/metadata v0.5.1/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k= cloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I= cloud.google.com/go/containeranalysis v0.6.0/go.mod h1:HEJoiEIu+lEXM+k7+qLCci0h33lX3ZqoYFdmPcoO7s4= cloud.google.com/go/datacatalog v1.3.0/go.mod h1:g9svFY6tuR+j+hrTw3J2dNcmI0dzmSiyOzm8kpLq0a0= @@ -109,8 +111,8 @@ cloud.google.com/go/gkehub v0.10.0/go.mod h1:UIPwxI0DsrpsVoWpLB0stwKCP+WFVG9+y97 cloud.google.com/go/grafeas v0.2.0/go.mod h1:KhxgtF2hb0P191HlY5besjYm6MqTSTj3LSI+M+ByZHc= cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= cloud.google.com/go/iam v0.5.0/go.mod h1:wPU9Vt0P4UmCux7mqtRu6jcpPAb74cP1fh50J3QpkUc= -cloud.google.com/go/iam v0.13.0 h1:+CmB+K0J/33d0zSQ9SlFWUeCCEn5XJA0ZMZ3pHE9u8k= -cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0= +cloud.google.com/go/iam v1.1.6 h1:bEa06k05IO4f4uJonbB5iAgKTPpABy1ayxaIZV/GHVc= +cloud.google.com/go/iam v1.1.6/go.mod h1:O0zxdPeGBoFdWW3HWmBxJsk0pfvNM/p/qa82rWOGTwI= cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic= cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI= cloud.google.com/go/lifesciences v0.5.0/go.mod h1:3oIKy8ycWGPUyZDR/8RNnTOYevhaMLqh5vLUXs9zvT8= @@ -154,6 +156,8 @@ cloud.google.com/go/retail v1.9.0/go.mod h1:g6jb6mKuCS1QKnH/dpu7isX253absFl6iE92 cloud.google.com/go/scheduler v1.4.0/go.mod h1:drcJBmxF3aqZJRhmkHQ9b3uSSpQoltBPGPxGAWROx6s= cloud.google.com/go/scheduler v1.5.0/go.mod h1:ri073ym49NW3AfT6DZi21vLZrG07GXr5p3H1KxN5QlI= cloud.google.com/go/secretmanager v1.6.0/go.mod h1:awVa/OXF6IiyaU1wQ34inzQNc4ISIDIrId8qE5QGgKA= +cloud.google.com/go/secretmanager v1.11.5 h1:82fpF5vBBvu9XW4qj0FU2C6qVMtj1RM/XHwKXUEAfYY= +cloud.google.com/go/secretmanager v1.11.5/go.mod h1:eAGv+DaCHkeVyQi0BeXgAHOU0RdrMeZIASKc+S7VqH4= cloud.google.com/go/security v1.5.0/go.mod h1:lgxGdyOKKjHL4YG3/YwIL2zLqMFCKs0UbQwgyZmfJl4= cloud.google.com/go/security v1.7.0/go.mod h1:mZklORHl6Bg7CNnnjLH//0UlAlaXqiG7Lb9PsPXLfD0= cloud.google.com/go/security v1.8.0/go.mod h1:hAQOwgmaHhztFhiQ41CjDODdWP0+AE1B3sX4OFlq+GU= @@ -171,8 +175,8 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9 cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc= cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s= -cloud.google.com/go/storage v1.29.0 h1:6weCgzRvMg7lzuUurI4697AqIRPU1SvzHhynwpW31jI= -cloud.google.com/go/storage v1.29.0/go.mod h1:4puEjyTKnku6gfKoTfNOU/W+a9JyuVNxjpS5GBrB8h4= +cloud.google.com/go/storage v1.39.1 h1:MvraqHKhogCOTXTlct/9C3K3+Uy2jBmFYb3/Sp6dVtY= +cloud.google.com/go/storage v1.39.1/go.mod h1:xK6xZmxZmo+fyP7+DEF6FhNc24/JAe95OLyOHCXFH1o= cloud.google.com/go/talent v1.1.0/go.mod h1:Vl4pt9jiHKvOgF9KoZo6Kob9oV4lwd/ZD5Cto54zDRw= cloud.google.com/go/talent v1.2.0/go.mod h1:MoNF9bhFQbiJ6eFD3uSsg0uBALw4n4gaCaEjBw9zo8g= cloud.google.com/go/videointelligence v1.6.0/go.mod h1:w0DIDlVRKtwPCn/C4iwZIJdvC69yInhW0cfi+p546uU= @@ -187,16 +191,43 @@ cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoIS dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0 h1:nyQWyZvwGTvunIMxi1Y9uXkcyr+I7TeNrr/foo4Kpk8= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0/go.mod h1:l38EPgmsp71HHLq9j7De57JcKOWPyhrsW1Awm1JS6K0= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 h1:tfLQ34V6F7tVSwoTf/4lH5sE0o6eCJuNDTmH09nDpbc= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY= +github.com/Azure/azure-sdk-for-go/sdk/keyvault/azsecrets v0.12.0 h1:xnO4sFyG8UH2fElBkcqLTOZsAajvKfnSlgBBW8dXYjw= +github.com/Azure/azure-sdk-for-go/sdk/keyvault/azsecrets v0.12.0/go.mod h1:XD3DIOOVgBCO03OleB1fHjgktVRFxlT++KwKgIOewdM= +github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1 h1:FbH3BbSb4bvGluTesZZ+ttN/MDsnMmQP36OSnDuSXqw= +github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1/go.mod h1:9V2j0jn9jDEkCkv8w/bKTNppX/d0FVA1ud77xCIP4KA= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v5 v5.0.0 h1:5n7dPVqsWfVKw+ZiEKSd3Kzu7gwBkbEBkeXb8rgaE9Q= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v5 v5.0.0/go.mod h1:HcZY0PHPo/7d75p99lB6lK0qYOP4vLRJUBpiehYXtLQ= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v6 v6.1.0 h1:FvuejXWdMIPK6sY0Tt3lgb45BCVybrvmmnGCEC7a1i4= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v6 v6.1.0/go.mod h1:drbnYtukMoZqUQq9hJASf41w3RB4VoTJPoPpe+XDHPU= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v2 v2.0.0 h1:PTFGRSlMKCQelWwxUyYVEUqseBJVemLyqWJjvMyt0do= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v2 v2.0.0/go.mod h1:LRr2FzBTQlONPPa5HREE5+RjSCTXl7BwOvYOaWTqCaI= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0 h1:2qsIIvxVT+uE6yrNldntJKlLRgxGbZ85kgtz5SNBhMw= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0/go.mod h1:AW8VEadnhw9xox+VaVd9sP7NjzOAnaZBLRH6Tq3cJ38= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault v1.4.0 h1:HlZMUZW8S4P9oob1nCHxCCKrytxyLc+24nUJGssoEto= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault v1.4.0/go.mod h1:StGsLbuJh06Bd8IBfnAlIFV3fLb+gkczONWf15hpX2E= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/managementgroups/armmanagementgroups v1.0.0 h1:pPvTJ1dY0sA35JOeFq6TsY2xj6Z85Yo23Pj4wCCvu4o= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/managementgroups/armmanagementgroups v1.0.0/go.mod h1:mLfWfj8v3jfWKsL9G4eoBoXVcsqcIUTapmdKy7uGOp0= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 h1:Dd+RhdJn0OTtVGaeDLZpcumkIVCtA/3/Fo42+eoYvVM= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0/go.mod h1:5kakwfW5CjC9KK+Q4wjXAg+ShuIm2mBMua0ZFj2C8PE= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= -github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= +github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= -github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g= github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= +github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= @@ -212,8 +243,8 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/aws/aws-sdk-go v1.44.122 h1:p6mw01WBaNpbdP2xrisz5tIkcNwzj/HysobNoaAHjgo= github.com/aws/aws-sdk-go v1.44.122/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= -github.com/aws/aws-sdk-go-v2 v1.24.1 h1:xAojnj+ktS95YZlDf0zxWBkbFtymPeDP+rvUQIH3uAU= -github.com/aws/aws-sdk-go-v2 v1.24.1/go.mod h1:LNh45Br1YAkEKaAqvmE1m8FUx6a5b/V0oAKV7of29b4= +github.com/aws/aws-sdk-go-v2 v1.32.6 h1:7BokKRgRPuGmKkFMhEg/jSul+tB9VvXhcViILtfG8b4= +github.com/aws/aws-sdk-go-v2 v1.32.6/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4 h1:OCs21ST2LrepDfD3lwlQiOqIGp6JiEUqG84GzTDoyJs= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4/go.mod h1:usURWEKSNNAcAZuzRn/9ZYPT8aZQkR7xcCtunK/LkJo= github.com/aws/aws-sdk-go-v2/config v1.26.3 h1:dKuc2jdp10y13dEEvPqWxqLoc0vF3Z9FC45MvuQSxOA= @@ -224,40 +255,40 @@ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 h1:c5I5iH+DZcH3xOIMlz3/tC github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11/go.mod h1:cRrYDYAMUohBJUtUnOhydaMHtiK/1NZ0Otc9lIb6O0Y= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.15.11 h1:I6lAa3wBWfCz/cKkOpAcumsETRkFAl70sWi8ItcMEsM= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.15.11/go.mod h1:be1NIO30kJA23ORBLqPo1LttEM6tPNSEcjkd1eKzNW0= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 h1:vF+Zgd9s+H4vOXd5BMaPWykta2a6Ih0AKLq/X6NYKn4= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10/go.mod h1:6BkRjejp/GR4411UGqkX8+wFMbFbqsUIimfK4XjOKR4= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 h1:nYPe006ktcqUji8S2mqXf9c/7NdiKriOwMvWQHgYztw= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10/go.mod h1:6UV4SZkVvmODfXKql4LCbaZUpF7HO2BX38FgBf9ZOLw= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.25 h1:s/fF4+yDQDoElYhfIVvSNyeCydfbuTKzhxSXDXCPasU= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.25/go.mod h1:IgPfDv5jqFIzQSNbUEMoitNooSMXjRSDkhXv8jiROvU= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.25 h1:ZntTCl5EsYnhN/IygQEUugpdwbhdkom9uHcbCftiGgA= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.25/go.mod h1:DBdPrgeocww+CSl1C8cEV8PN1mHMBhuCDLpXezyvWkE= github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 h1:GrSw8s0Gs/5zZ0SX+gX4zQjRnRsMJDJ2sLur1gRBhEM= github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY= github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.10 h1:5oE2WzJE56/mVveuDZPJESKlg/00AaS2pY2QZcnxg4M= github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.10/go.mod h1:FHbKWQtRBYUz4vO5WBWjzMD2by126ny5y/1EoaWoLfI= -github.com/aws/aws-sdk-go-v2/service/dynamodb v1.26.8 h1:XKO0BswTDeZMLDBd/b5pCEZGttNXrzRUVtFvp2Ak/Vo= -github.com/aws/aws-sdk-go-v2/service/dynamodb v1.26.8/go.mod h1:N5tqZcYMM0N1PN7UQYJNWuGyO886OfnMhf/3MAbqMcI= -github.com/aws/aws-sdk-go-v2/service/ecr v1.24.7 h1:3iaT/LnGV6jNtbBkvHZDlzz7Ky3wMHDJAyFtGd5GUJI= -github.com/aws/aws-sdk-go-v2/service/ecr v1.24.7/go.mod h1:mtzCLxk6M+KZbkJdq3cUH9GCrudw8qCy5C3EHO+5vLc= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.187.0 h1:cA4hWo269CN5RY7Arqt8BfzXF0KIN8DSNo/KcqHKkWk= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.187.0/go.mod h1:ossaD9Z1ugYb6sq9QIqQLEOorCGcqUoxlhud9M9yE70= +github.com/aws/aws-sdk-go-v2/service/eks v1.51.1 h1:OQjVHkANBbwE055NK49M/kelQbapsQOsSfUUWP1mi3w= +github.com/aws/aws-sdk-go-v2/service/eks v1.51.1/go.mod h1:9wMtzHTjYbK5MLzYBWSznUPsys/n9LapMwb6UhKOVPQ= github.com/aws/aws-sdk-go-v2/service/iam v1.28.7 h1:FKPRDYZOO0Eur19vWUL1B40Op0j89KQj3kARjrszMK8= github.com/aws/aws-sdk-go-v2/service/iam v1.28.7/go.mod h1:YzMYyQ7S4twfYzLjwP24G1RAxypozVZeNaG1r2jxRms= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 h1:/b31bi3YVNlkzkBrm9LfpaKoaYZUxIAj4sHfOTmLfqw= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4/go.mod h1:2aGXHFmbInwgP9ZfpmdIfOELL79zhdNYNmReK8qDfdQ= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 h1:TToQNkvGguu209puTojY/ozlqy2d/SFNcoLIqTFi42g= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0/go.mod h1:0jp+ltwkf+SwG2fm/PKo8t4y8pJSgOCO4D8Lz3k0aHQ= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.10 h1:L0ai8WICYHozIKK+OtPzVJBugL7culcuM4E4JOpIEm8= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.10/go.mod h1:byqfyxJBshFk0fF9YmK0M0ugIO8OWjzH2T3bPG4eGuA= -github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.8.11 h1:e9AVb17H4x5FTE5KWIP5M1Du+9M86pS+Hw0lBUdN8EY= -github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.8.11/go.mod h1:B90ZQJa36xo0ph9HsoteI1+r8owgQH/U1QNfqZQkj1Q= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 h1:DBYTXwIGQSGs9w4jKm60F5dmCQ3EEruxdc0MFh+3EY4= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10/go.mod h1:wohMUQiFdzo0NtxbBg0mSRGZ4vL3n0dKjLTINdcIino= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.3 h1:qcxX0JYlgWH3hpPUnd6U0ikcl6LLA9sLkXE2w1fpMvY= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.3/go.mod h1:cLSNEmI45soc+Ef8K/L+8sEA3A3pYFEYf5B5UI+6bH4= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.10 h1:KOxnQeWy5sXyS37fdKEvAsGHOr9fa/qvwxfJurR/BzE= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.10/go.mod h1:jMx5INQFYFYB3lQD9W0D8Ohgq6Wnl7NYOJ2TQndbulI= github.com/aws/aws-sdk-go-v2/service/s3 v1.48.0 h1:PJTdBMsyvra6FtED7JZtDpQrIAflYDHFoZAu/sKYkwU= github.com/aws/aws-sdk-go-v2/service/s3 v1.48.0/go.mod h1:4qXHrG1Ne3VGIMZPCB8OjH/pLFO94sKABIusjh0KWPU= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.34.7 h1:Nyfbgei75bohfmZNxgN27i528dGYVzqWJGlAO6lzXy8= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.34.7/go.mod h1:FG4p/DciRxPgjA+BEOlwRHN0iA8hX2h9g5buSy3cTDA= github.com/aws/aws-sdk-go-v2/service/sso v1.18.6 h1:dGrs+Q/WzhsiUKh82SfTVN66QzyulXuMDTV/G8ZxOac= github.com/aws/aws-sdk-go-v2/service/sso v1.18.6/go.mod h1:+mJNDdF+qiUlNKNC3fxn74WWNN+sOiGOEImje+3ScPM= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.6 h1:Yf2MIo9x+0tyv76GljxzqA3WtC5mw7NmazD2chwjxE4= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.6/go.mod h1:ykf3COxYI0UJmxcfcxcVuz7b6uADi1FkiUz6Eb7AgM8= github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 h1:NzO4Vrau795RkUdSHKEwiR01FaGzGOH1EETJ+5QHnm0= github.com/aws/aws-sdk-go-v2/service/sts v1.26.7/go.mod h1:6h2YuIoxaMSCFf5fi1EgZAwdfkGMgDY+DVfa61uLe4U= -github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM= -github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE= +github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro= +github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas= github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= @@ -271,8 +302,9 @@ github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWR github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs= github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= +github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= +github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= @@ -287,10 +319,13 @@ github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= +github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU= +github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -304,10 +339,16 @@ github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go. github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= +github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY= github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4= @@ -322,8 +363,25 @@ github.com/go-git/go-git/v5 v5.11.0/go.mod h1:6GFcX2P3NM7FPBfpePbpLd21XxsgdAt+lK github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho= github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -356,11 +414,13 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -382,6 +442,9 @@ github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4r github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -403,18 +466,21 @@ github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 h1:5iH8iuqE5apketRbSFBy+X1V0o+l+8NF1avt4HWl7cA= +github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/s2a-go v0.1.4 h1:1kZ/sQM3srePvKs3tXAvQzo66XfcReoqFpIpIccE7Oc= -github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A= +github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM= +github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= -github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k= -github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= +github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw= +github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= @@ -424,38 +490,46 @@ github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99 github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo= github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY= -github.com/googleapis/gax-go/v2 v2.11.0 h1:9V9PWXEsWnPpQhu/PeQIkS4eGzMlTLGgt80cUUI8Ki4= -github.com/googleapis/gax-go/v2 v2.11.0/go.mod h1:DxmR61SGKkGLa2xigwuZIQpkCI2S5iydzRfb3peWZJI= +github.com/googleapis/gax-go/v2 v2.12.3 h1:5/zPPDvw8Q1SuXjrqrZslrqT7dL/uJT2CQii/cLCKqA= +github.com/googleapis/gax-go/v2 v2.12.3/go.mod h1:AKloxT6GtNbaLm8QTNSidHUVsHYcBHwWRvkNFJUQcS4= github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= -github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= -github.com/hashicorp/go-getter v1.7.3 h1:bN2+Fw9XPFvOCjB0UOevFIMICZ7G2XSQHzfvLUyOM5E= -github.com/hashicorp/go-getter v1.7.3/go.mod h1:W7TalhMmbPmsSMdNjD0ZskARur/9GJ17cfHTRtXV744= +github.com/hashicorp/go-getter v1.7.5 h1:dT58k9hQ/vbxNMwoI5+xFYAJuv6152UNvdHokfI5wE4= +github.com/hashicorp/go-getter v1.7.5/go.mod h1:W7TalhMmbPmsSMdNjD0ZskARur/9GJ17cfHTRtXV744= github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo= github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I= github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 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/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4= github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU= +github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= +github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.15.11 h1:Lcadnb3RKGin4FYM/orgq0qde+nc15E5Cbqg4B9Sx9c= github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= @@ -466,32 +540,57 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= -github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= +github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= -github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= -github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/onsi/ginkgo/v2 v2.20.2 h1:7NVCeyIWROIAheY21RLS+3j2bb52W0W82tkberYytp4= +github.com/onsi/ginkgo/v2 v2.20.2/go.mod h1:K9gyxPIlb+aIvnZ8bd9Ak+YP18w3APlR+5coaZoE2ag= +github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8= +github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8= +github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= @@ -500,20 +599,25 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= -github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/skeema/knownhosts v1.2.1 h1:SHWdIUa82uGZz+F+47k8SY4QhhI291cXCpopT1lK2AQ= github.com/skeema/knownhosts v1.2.1/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= github.com/slack-go/slack v0.12.3 h1:92/dfFU8Q5XP6Wp5rr5/T5JHLM5c5Smtn53fhToAP88= github.com/slack-go/slack v0.12.3/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -521,12 +625,14 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8= github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho= github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= @@ -546,26 +652,37 @@ go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= +go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= +go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= +go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= +go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= +go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw= +go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc= +go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= +go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= -go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= -go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= -go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= -go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= -golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -603,8 +720,8 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= -golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= +golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -656,8 +773,8 @@ golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= +golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= 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= @@ -683,8 +800,8 @@ golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= golang.org/x/oauth2 v0.1.0/go.mod h1:G9FE4dLTsbXUu90h/Pf85g4w1D+SSAgR+q46nJZ8M4A= -golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= -golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= +golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= +golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -700,8 +817,8 @@ golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -768,21 +885,22 @@ golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= -golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= +golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 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= @@ -792,15 +910,16 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 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= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -838,6 +957,7 @@ golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= @@ -846,6 +966,7 @@ golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= @@ -854,8 +975,8 @@ golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= -golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= +golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= +golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -863,8 +984,9 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -913,15 +1035,14 @@ google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= google.golang.org/api v0.98.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= google.golang.org/api v0.100.0/go.mod h1:ZE3Z2+ZOr87Rx7dqFsdRQkRBk36kDtp/h+QpHbB7a70= -google.golang.org/api v0.126.0 h1:q4GJq+cAdMAC7XP7njvQ4tvohGLiSlytuL4BQxbIZ+o= -google.golang.org/api v0.126.0/go.mod h1:mBwVAtz+87bEN6CbA1GtZPDOqY2R5ONPqJeIlvyo4Aw= +google.golang.org/api v0.177.0 h1:8a0p/BbPa65GlqGWtUKxot4p0TV8OGOfyTjtmkXNXmk= +google.golang.org/api v0.177.0/go.mod h1:srbhue4MLjkjbkux5p3dw/ocYOSZTaIEvf7bCOnFQDw= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -1024,12 +1145,12 @@ google.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e/go.mod h1:3526vdqw google.golang.org/genproto v0.0.0-20221014173430-6e2ab493f96b/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= google.golang.org/genproto v0.0.0-20221025140454-527a21cfbd71/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= -google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc h1:8DyZCyvI8mE1IdLy/60bS+52xfymkE72wv1asokgtao= -google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:xZnkP7mREFX5MORlOPEzLMr+90PPZQ2QWzrVTWfAq64= -google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc h1:kVKPf/IiYSBWEWtkIn6wZXwWGCnLKcC8oWfZvXjsGnM= -google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc h1:XSJ8Vk1SWuNr8S18z1NZSziL0CPIXLCCMDOEFtHBOFc= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= +google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y= +google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:mqHbVIp48Muh7Ywss/AD6I5kNVKZMmAa/QEW58Gxp2s= +google.golang.org/genproto/googleapis/api v0.0.0-20240610135401-a8a62080eff3 h1:QW9+G6Fir4VcRXVH8x3LilNAb6cxBGLa6+GM4hRwexE= +google.golang.org/genproto/googleapis/api v0.0.0-20240610135401-a8a62080eff3/go.mod h1:kdrSS/OiLkPrNUpzD4aHgCq2rVuC/YRxok32HXZ4vRE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -1065,8 +1186,8 @@ google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACu google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= -google.golang.org/grpc v1.55.0 h1:3Oj82/tFSCeUrRTg/5E/7d/W5A1tj6Ky1ABAuZuv5ag= -google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8= +google.golang.org/grpc v1.66.2 h1:3QdXkuq3Bkh7w+ywLdLvM56cmGvQHUMZpiCzt6Rqaoo= +google.golang.org/grpc v1.66.2/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -1083,8 +1204,8 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= -google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -1092,14 +1213,17 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/cheggaaa/pb.v1 v1.0.27/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= @@ -1110,8 +1234,26 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +k8s.io/api v0.31.1 h1:Xe1hX/fPW3PXYYv8BlozYqw63ytA92snr96zMW9gWTU= +k8s.io/api v0.31.1/go.mod h1:sbN1g6eY6XVLeqNsZGLnI5FwVseTrZX7Fv3O26rhAaI= +k8s.io/apimachinery v0.31.1 h1:mhcUBbj7KUjaVhyXILglcVjuS4nYXiwC+KKFBgIVy7U= +k8s.io/apimachinery v0.31.1/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= +k8s.io/client-go v0.31.1 h1:f0ugtWSbWpxHR7sjVpQwuvw9a3ZKLXX0u0itkFXufb0= +k8s.io/client-go v0.31.1/go.mod h1:sKI8871MJN2OyeqRlmA4W4KM9KBdBUpDLu/43eGemCg= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20240521193020-835d969ad83a h1:zD1uj3Jf+mD4zmA7W+goE5TxDkI7OGJjBNBzq5fJtLA= +k8s.io/kube-openapi v0.0.0-20240521193020-835d969ad83a/go.mod h1:UxDHUPsUwTOOxSU+oXURfFBcAS6JwiRXTYqYwfuGowc= +k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A= +k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= mvdan.cc/sh/v3 v3.8.0 h1:ZxuJipLZwr/HLbASonmXtcvvC9HXY9d2lXZHnKGjFc8= mvdan.cc/sh/v3 v3.8.0/go.mod h1:w04623xkgBVo7/IUK89E0g8hBykgEpN0vgOj3RJr6MY= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/main.go b/main.go index c87d279..26ce1cb 100644 --- a/main.go +++ b/main.go @@ -9,7 +9,7 @@ import ( "github.com/urfave/cli/v2" "go.uber.org/zap" - "rmk/commands" + "rmk/cmd" "rmk/logger" ) @@ -27,7 +27,7 @@ var ( func init() { var output string - for _, val := range commands.FlagsGlobal() { + for _, val := range cmd.FlagsGlobal() { output += fmt.Sprintf(" %s\n", val.String()) } @@ -95,7 +95,7 @@ func runCLI() *cli.App { "version": version, } - app.Flags = commands.FlagsGlobal() + app.Flags = cmd.FlagsGlobal() app.Before = func(c *cli.Context) error { logger.Init(c.String("log-format"), c.String("log-level")) return nil @@ -107,7 +107,7 @@ func runCLI() *cli.App { // Enable flag and command suggestions app.Suggest = true - app.Commands = commands.Commands() + app.Commands = cmd.Commands() sort.Sort(cli.CommandsByName(app.Commands)) return app diff --git a/providers/aws_provider/aws.go b/providers/aws_provider/aws.go new file mode 100644 index 0000000..ca975c8 --- /dev/null +++ b/providers/aws_provider/aws.go @@ -0,0 +1,889 @@ +package aws_provider + +import ( + "bytes" + "context" + "encoding/base64" + "errors" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "reflect" + "strconv" + "strings" + "text/template" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/feature/s3/manager" + "github.com/aws/aws-sdk-go-v2/service/ec2" + ec2Type "github.com/aws/aws-sdk-go-v2/service/ec2/types" + "github.com/aws/aws-sdk-go-v2/service/eks" + eksType "github.com/aws/aws-sdk-go-v2/service/eks/types" + "github.com/aws/aws-sdk-go-v2/service/iam" + "github.com/aws/aws-sdk-go-v2/service/s3" + s3Type "github.com/aws/aws-sdk-go-v2/service/s3/types" + "github.com/aws/aws-sdk-go-v2/service/secretsmanager" + smType "github.com/aws/aws-sdk-go-v2/service/secretsmanager/types" + "github.com/aws/aws-sdk-go-v2/service/sts" + "github.com/aws/smithy-go" + "go.uber.org/zap" + "k8s.io/client-go/tools/clientcmd" + "k8s.io/client-go/tools/clientcmd/api" + + "rmk/util" +) + +const ( + AWSClusterProvider = "aws" + + AWSConfigTemplateFile = `[profile {{ .Profile }}] +region = {{ .Region }} +output = {{ .Output }} +` + + AWSCredentialsTemplateFile = `[{{ .Profile }}] +aws_access_key_id = {{ .AwsCredentialsProfile.AccessKeyID }} +aws_secret_access_key = {{ .AwsCredentialsProfile.SecretAccessKey }} +{{- if .AwsCredentialsProfile.SessionToken }} +aws_session_token = {{ .AwsCredentialsProfile.SessionToken }} +{{- end }} +` + + apiErrorAccessDeniedException = "AccessDeniedException" +) + +type AwsConfigure struct { + *MFAToken `yaml:"-"` + AccountID string `yaml:"account_id,omitempty"` + AwsCredentialsProfile `yaml:"-"` + ConfigSource string `yaml:"config-source"` + CredentialsSource string `yaml:"credentials-source"` + Ctx context.Context `yaml:"-"` + IAMUserName string `yaml:"user-name,omitempty"` + MFADeviceSerialNumber string `yaml:"mfa-device,omitempty"` + MFAProfileCredentials aws.Credentials `yaml:"-"` + Output string `yaml:"output,omitempty"` + Profile string `yaml:"profile,omitempty"` + Region string `json:"aws-region,omitempty" yaml:"region,omitempty"` +} + +type MFAToken struct { + AccessKeyID string + Expiration time.Time + SecretAccessKey string + SessionToken string +} + +type AwsCredentialsProfile struct { + AccessKeyID string `json:"aws-access-key-id,omitempty" yaml:"-"` + SecretAccessKey string `json:"aws-secret-access-key,omitempty" yaml:"-"` + SessionToken string `yaml:"-"` +} + +func (a *AwsConfigure) AWSSharedConfigFile(profile string) []string { + return []string{config.DefaultSharedConfigFilename() + "_" + profile} +} + +func (a *AwsConfigure) AWSSharedCredentialsFile(profile string) []string { + return []string{config.DefaultSharedCredentialsFilename() + "_" + profile} +} + +func NewAwsConfigure(ctx context.Context, profile string) *AwsConfigure { + return &AwsConfigure{Ctx: ctx, Output: "text", Profile: profile} +} + +func (a *AwsConfigure) errorProxy(cfg aws.Config, err error) (aws.Config, error) { + for _, val := range cfg.ConfigSources { + switch result := val.(type) { + case config.SharedConfig: + if len(result.Profile) == 0 && len(result.Region) == 0 { + return aws.Config{}, fmt.Errorf("AWS profile by name %s does not exist, will be created", a.Profile) + } + } + } + + return cfg, err +} + +// configOptions - forming custom paths to AWS credentials and profile +func (a *AwsConfigure) configOptions() []func(options *config.LoadOptions) error { + return []func(options *config.LoadOptions) error{ + config.WithSharedConfigFiles(a.AWSSharedConfigFile(a.Profile)), + config.WithSharedCredentialsFiles(a.AWSSharedCredentialsFile(a.Profile)), + config.WithSharedConfigProfile(a.Profile), + } +} + +func getTagStructName(i interface{}, name string) error { + if field, ok := reflect.TypeOf(i).Elem().FieldByName(name); ok { + return fmt.Errorf("profile option %s required", strings.TrimSuffix(field.Tag.Get("json"), ",omitempty")) + } else { + return fmt.Errorf("field with name %s not defined", name) + } +} + +// ValidateAWSCredentials will validate the required parameters for AWS authentication +func (a *AwsConfigure) ValidateAWSCredentials() error { + if len(a.AwsCredentialsProfile.AccessKeyID) == 0 { + return getTagStructName(&a.AwsCredentialsProfile, "AccessKeyID") + } + + if len(a.AwsCredentialsProfile.SecretAccessKey) == 0 { + return getTagStructName(&a.AwsCredentialsProfile, "SecretAccessKey") + } + + if len(a.Region) == 0 { + return getTagStructName(a, "Region") + } + + return nil +} + +// RenderAWSConfigProfile will render the AWS profile. +func (a *AwsConfigure) RenderAWSConfigProfile(temp string) ([]byte, error) { + tmpl, err := template.New("AWS config Profile").Parse(temp) + if err != nil { + return nil, err + } + + var credsFileStr bytes.Buffer + err = tmpl.Execute(&credsFileStr, a) + if err != nil { + return nil, err + } + + return credsFileStr.Bytes(), nil +} + +// RenderBase64EncodedAWSConfigProfile will render the AWS profile, encoded in base 64. +func (a *AwsConfigure) RenderBase64EncodedAWSConfigProfile(temp string) (string, error) { + configProfile, err := a.RenderAWSConfigProfile(temp) + if err != nil { + return "", err + } + + return base64.StdEncoding.EncodeToString(configProfile), nil +} + +func (a *AwsConfigure) ReadAWSConfigProfile() error { + cfg, err := config.LoadDefaultConfig(a.Ctx, a.configOptions()...) + if err != nil { + return err + } + + for _, val := range cfg.ConfigSources { + switch result := val.(type) { + case config.SharedConfig: + a.AwsCredentialsProfile.AccessKeyID = result.Credentials.AccessKeyID + a.AwsCredentialsProfile.SecretAccessKey = result.Credentials.SecretAccessKey + a.AwsCredentialsProfile.SessionToken = result.Credentials.SessionToken + a.Region = result.Region + } + } + + return nil +} + +func (a *AwsConfigure) WriteAWSConfigProfile() error { + var ( + err error + sharedFiles = make(map[string][]byte) + ) + + sharedFiles[a.ConfigSource], err = a.RenderAWSConfigProfile(AWSConfigTemplateFile) + if err != nil { + return err + } + + sharedFiles[a.CredentialsSource], err = a.RenderAWSConfigProfile(AWSCredentialsTemplateFile) + if err != nil { + return err + } + + for key, val := range sharedFiles { + if err := os.MkdirAll(filepath.Dir(key), 0755); err != nil { + return err + } + + if err := os.WriteFile(key, val, 0644); err != nil { + return err + } + } + + return nil +} + +func (a *AwsConfigure) GetAWSUserName() error { + cfg, err := a.errorProxy(config.LoadDefaultConfig(a.Ctx, a.configOptions()...)) + if err != nil { + return err + } + + user, err := iam.NewFromConfig(cfg).GetUser(a.Ctx, &iam.GetUserInput{}) + if err != nil { + return err + } + + a.IAMUserName = aws.ToString(user.User.UserName) + + return nil +} + +func (a *AwsConfigure) GetAWSMFACredentials() error { + cfg, err := a.errorProxy(config.LoadDefaultConfig(a.Ctx, a.configOptions()...)) + if err != nil { + return err + } + + if a.MFAProfileCredentials, err = cfg.Credentials.Retrieve(a.Ctx); err != nil { + return err + } + + return nil +} + +func (a *AwsConfigure) GetAWSMFADevicesSerialNumbers() error { + var serialNumbers = make(map[string]string) + + if err := a.GetAWSUserName(); err != nil { + return err + } + + if err := a.GetAWSMFACredentials(); err != nil { + return err + } + + cfg, err := a.errorProxy(config.LoadDefaultConfig(a.Ctx, a.configOptions()...)) + if err != nil { + return err + } + + mfaDevices, err := iam.NewFromConfig(cfg).ListMFADevices(a.Ctx, + &iam.ListMFADevicesInput{UserName: aws.String(a.IAMUserName)}) + if err != nil { + return err + } + + if len(mfaDevices.MFADevices) > 1 { + for key, val := range mfaDevices.MFADevices { + fmt.Printf("%d. - MFA Device SerialNumber: %s\n", key+1, aws.ToString(val.SerialNumber)) + serialNumbers[strconv.Itoa(key+1)] = aws.ToString(val.SerialNumber) + } + + if _, ok := serialNumbers[util.ReadStdin("number SerialNumber")]; ok { + a.MFADeviceSerialNumber = serialNumbers[util.ReadStdin("number SerialNumber")] + } else { + return fmt.Errorf("incorrectly specified number SerialNumber") + } + } else if len(mfaDevices.MFADevices) == 1 { + a.MFADeviceSerialNumber = aws.ToString(mfaDevices.MFADevices[0].SerialNumber) + } + + return nil +} + +func (a *AwsConfigure) GetAWSMFASessionToken() error { + cfg, err := a.errorProxy(config.LoadDefaultConfig(a.Ctx, a.configOptions()...)) + if err != nil { + return err + } + + if err := a.GetAWSMFADevicesSerialNumbers(); err != nil { + return err + } + + token, err := sts.NewFromConfig(cfg).GetSessionToken(a.Ctx, &sts.GetSessionTokenInput{ + DurationSeconds: aws.Int32(43200), + SerialNumber: aws.String(a.MFADeviceSerialNumber), + TokenCode: aws.String(util.ReadStdin("TOTP")), + }) + if err != nil { + return err + } + + a.MFAToken = &MFAToken{ + AccessKeyID: aws.ToString(token.Credentials.AccessKeyId), + Expiration: aws.ToTime(token.Credentials.Expiration), + SecretAccessKey: aws.ToString(token.Credentials.SecretAccessKey), + SessionToken: aws.ToString(token.Credentials.SessionToken), + } + + return nil +} + +func (a *AwsConfigure) GetAwsConfigure(profile string) (bool, error) { + cfg, err := a.errorProxy(config.LoadDefaultConfig(a.Ctx, + config.WithSharedConfigFiles(a.AWSSharedConfigFile(profile)), + config.WithSharedCredentialsFiles(a.AWSSharedCredentialsFile(profile)), + config.WithSharedConfigProfile(profile), + )) + if err != nil { + return true, err + } + + client := sts.NewFromConfig(cfg) + identity, err := client.GetCallerIdentity(a.Ctx, &sts.GetCallerIdentityInput{}) + if err != nil { + return false, err + } + + a.Region = cfg.Region + a.AccountID = aws.ToString(identity.Account) + + return true, nil +} + +func (a *AwsConfigure) GetAWSClusterContext(clusterName string) ([]byte, error) { + cfg, err := a.errorProxy(config.LoadDefaultConfig(a.Ctx, a.configOptions()...)) + if err != nil { + return nil, err + } + + client := eks.NewFromConfig(cfg) + cluster, err := client.DescribeCluster(a.Ctx, &eks.DescribeClusterInput{Name: aws.String(clusterName)}) + if err != nil { + return nil, fmt.Errorf("kubecontext for %s provider's %s cluster not found", + strings.ToUpper(AWSClusterProvider), clusterName) + } + + return a.generateUserKubeconfig(cluster.Cluster) +} + +func (a *AwsConfigure) generateUserKubeconfig(cluster *eksType.Cluster) ([]byte, error) { + var execEnvVars []api.ExecEnvVar + + clusterName := aws.ToString(cluster.Name) + userName := a.getKubeConfigUserName(clusterName) + + cfg, err := a.generateBaseKubeConfig(cluster) + if err != nil { + return nil, fmt.Errorf("creating base kubeconfig: %w", err) + } + + execEnvVars = append(execEnvVars, + api.ExecEnvVar{Name: "AWS_PROFILE", Value: a.Profile}, + api.ExecEnvVar{Name: "AWS_CONFIG_FILE", Value: strings.Join(a.AWSSharedConfigFile(a.Profile), "")}, + api.ExecEnvVar{Name: "AWS_SHARED_CREDENTIALS_FILE", Value: strings.Join(a.AWSSharedCredentialsFile(a.Profile), "")}, + ) + + // Version v1alpha1 was removed in Kubernetes v1.23. + // Version v1 was released in Kubernetes v1.23. + // Version v1beta1 was selected as it has the widest range of support + // This should be changed to v1 once EKS no longer supports Kubernetes 0 { + cfg.Region = region + } + + client := s3.NewFromConfig(cfg) + _, err = client.HeadObject(ctx, &s3.HeadObjectInput{ + Bucket: aws.String(bucketName), + Key: aws.String(key), + }) + if err != nil { + var responseError *awshttp.ResponseError + if errors.As(err, &responseError) && responseError.ResponseError.HTTPStatusCode() == http.StatusNotFound { + return false, nil + } + + return false, err + } + + return true, nil +} + +// S3ListObjectsAPI defines the interface for the ListObjectsV2 function. +// We use this interface to test the function using a mocked service. +type S3ListObjectsAPI interface { + ListObjectsV2(ctx context.Context, + params *s3.ListObjectsV2Input, + optFns ...func(*s3.Options)) (*s3.ListObjectsV2Output, error) +} + +// GetObjects retrieves the objects in an Amazon Simple Storage Service (Amazon S3) bucket +// Inputs: +// +// c is the context of the method call, which includes the AWS Region +// api is the interface that defines the method call +// input defines the input arguments to the service call. +// +// Output: +// +// If success, a ListObjectsV2Output object containing the result of the service call and nil +// Otherwise, nil and an error from the call to ListObjectsV2 +func GetObjects(c context.Context, api S3ListObjectsAPI, input *s3.ListObjectsV2Input) (*s3.ListObjectsV2Output, error) { + return api.ListObjectsV2(c, input) +} + +func (a *AwsConfigure) DownloadFromAWSBucket(region, bucketName, localDir, filePrefix string) error { + var noSuchBucket *s3Type.NoSuchBucket + + downloadToFile := func(downloader *manager.Downloader, targetDirectory, bucket, key string) error { + // Create the directories in the path + file := filepath.Join(targetDirectory, key) + if err := os.MkdirAll(filepath.Dir(file), 0775); err != nil { + return err + } + + fd, err := os.Create(file) + if err != nil { + return err + } + defer func(fd *os.File) { + err := fd.Close() + if err != nil { + + } + }(fd) + + // Download the file using the AWS SDK for Go + zap.S().Infof("downloading s3://%s/%s to %s", bucket, key, file) + _, err = downloader.Download(context.TODO(), fd, &s3.GetObjectInput{Bucket: &bucket, Key: &key}) + + return err + } + + ctx := context.TODO() + cfg, err := a.errorProxy(config.LoadDefaultConfig(ctx, a.configOptions()...)) + if err != nil { + return err + } + + // needed for specific AWS account where S3 used + if len(region) > 0 { + cfg.Region = region + } + + client := s3.NewFromConfig(cfg) + m := manager.NewDownloader(client) + paginator := s3.NewListObjectsV2Paginator(client, &s3.ListObjectsV2Input{Bucket: aws.String(bucketName)}) + + for paginator.HasMorePages() { + page, err := paginator.NextPage(context.TODO()) + if err != nil { + if errors.As(err, &noSuchBucket) { + return fmt.Errorf("specified bucket %s does not exist", bucketName) + } + + return err + } + + for _, obj := range page.Contents { + if strings.HasPrefix(aws.ToString(obj.Key), filePrefix) { + if err := downloadToFile(m, localDir, bucketName, aws.ToString(obj.Key)); err != nil { + return err + } + } + } + } + + input := &s3.ListObjectsV2Input{ + Bucket: aws.String(bucketName), + } + + resp, err := GetObjects(context.TODO(), client, input) + if err != nil { + return err + } + + if len(resp.Contents) == 0 { + zap.S().Warnf("S3 bucket %s is empty, files do not exist", bucketName) + return nil + } + + return nil +} + +func (a *AwsConfigure) GetAWSBucketFileData(bucketName, key string) ([]byte, error) { + var client *s3.Client + ctx := context.TODO() + if len(a.Profile) > 0 { + cfg, err := a.errorProxy(config.LoadDefaultConfig(ctx, a.configOptions()...)) + if err != nil { + return nil, err + } + + client = s3.NewFromConfig(cfg) + } else { + client = s3.NewFromConfig(aws.Config{Region: a.Region}) + } + + input := &s3.GetObjectInput{ + Bucket: aws.String(bucketName), + Key: aws.String(key), + } + + resp, err := client.GetObject(ctx, input) + if err != nil { + return nil, err + } + + return io.ReadAll(resp.Body) +} + +func (a *AwsConfigure) UploadToAWSBucket(bucketName, localDir, pattern string) error { + ctx := context.TODO() + cfg, err := a.errorProxy(config.LoadDefaultConfig(ctx, a.configOptions()...)) + if err != nil { + return err + } + + uploader := manager.NewUploader(s3.NewFromConfig(cfg)) + + match, err := util.WalkMatch(localDir, pattern) + if err != nil { + return err + } + + for _, path := range match { + if filepath.Base(path) != util.SopsAgeKeyFile { + data, err := os.ReadFile(path) + if err != nil { + return err + } + + result, err := uploader.Upload(context.TODO(), &s3.PutObjectInput{ + Bucket: aws.String(bucketName), + Key: aws.String(filepath.Base(path)), + Body: bytes.NewReader(data), + }) + if err != nil { + var responseError *awshttp.ResponseError + if errors.As(err, &responseError) && responseError.ResponseError.HTTPStatusCode() == http.StatusNotFound { + return fmt.Errorf("specified bucket %s does not exist", bucketName) + } + + if errors.As(err, &responseError) && responseError.ResponseError.HTTPStatusCode() == http.StatusForbidden { + return fmt.Errorf("you are not permitted to upload SOPS age keys for specified bucket %s, "+ + "access denied", bucketName) + } + + return err + } + + zap.S().Infof("uploading %s... to %s", path, result.Location) + } + } + + return nil +} diff --git a/providers/azure_provider/azure.go b/providers/azure_provider/azure.go new file mode 100644 index 0000000..ca3f20f --- /dev/null +++ b/providers/azure_provider/azure.go @@ -0,0 +1,376 @@ +package azure_provider + +import ( + "crypto/sha1" + "encoding/json" + "errors" + "fmt" + "os" + "reflect" + "strings" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/Azure/azure-sdk-for-go/sdk/keyvault/azsecrets" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v6" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources" + "go.uber.org/zap" + "golang.org/x/net/context" + + "rmk/util" +) + +const ( + AzureClusterProvider = "azure" + AzureHomeDir = "." + AzureClusterProvider + AzurePrefix = "service-principal-credentials_" +) + +type AzureRawServicePrincipal struct { + AppId string `json:"appId"` + DisplayName string `json:"displayName"` + Password string `json:"password"` + Tenant string `json:"tenant"` +} + +type AzureKeyVault struct { + KeyVaultName string `json:"-" yaml:"key-vault-name,omitempty"` + KeyVaultURI string `json:"-" yaml:"key-vault-uri,omitempty"` + ResourceGroupName string `json:"-" yaml:"resource-group-name,omitempty"` +} + +type AzureClient struct { + Credentials *azidentity.ClientSecretCredential `json:"-" yaml:"-"` + Ctx context.Context `json:"-" yaml:"-"` + GroupClient *armresources.ResourceGroupsClient `json:"-" yaml:"-"` + ManagedClustersClient *armcontainerservice.ManagedClustersClient `json:"-" yaml:"-"` + VaultsClient *armkeyvault.VaultsClient `json:"-" yaml:"-"` +} + +type AzureConfigure struct { + AzureClient `json:"-" yaml:"-"` + AzureKeyVault `json:"-" yaml:"key-vault,omitempty"` + ClientID string `json:"client-id,omitempty" yaml:"-"` + ClientSecret string `json:"client-secret,omitempty" yaml:"-"` + Location string `json:"location,omitempty" yaml:"location,omitempty"` + SubscriptionID string `json:"subscription-id,omitempty" yaml:"subscription-id,omitempty"` + TenantID string `json:"tenant-id,omitempty" yaml:"-"` +} + +func NewAzureConfigure() *AzureConfigure { + return &AzureConfigure{} +} + +func NewRawSP() *AzureRawServicePrincipal { + return &AzureRawServicePrincipal{} +} + +func (ac *AzureConfigure) MergeAzureRawSP(asp *AzureRawServicePrincipal) { + ac.ClientID = asp.AppId + ac.ClientSecret = asp.Password + ac.TenantID = asp.Tenant +} + +func getTagStructName(i interface{}, name string) error { + if field, ok := reflect.TypeOf(i).Elem().FieldByName(name); ok { + return fmt.Errorf("service principal option %s required", strings.TrimSuffix(field.Tag.Get("json"), ",omitempty")) + } else { + return fmt.Errorf("field with name %s not defined", name) + } +} + +func generateKeyVaultName(tenant string) string { + return "kv-" + fmt.Sprintf("%x", sha1.Sum([]byte(tenant)))[0:16] +} + +func (ac *AzureConfigure) ValidateSPCredentials() error { + if len(ac.ClientID) == 0 { + return getTagStructName(ac, "ClientID") + } + + if len(ac.ClientSecret) == 0 { + return getTagStructName(ac, "ClientSecret") + } + + if len(ac.Location) == 0 { + return getTagStructName(ac, "Location") + } + + if len(ac.SubscriptionID) == 0 { + return getTagStructName(ac, "SubscriptionID") + } + + if len(ac.TenantID) == 0 { + return getTagStructName(ac, "TenantID") + } + + return nil +} + +func (ac *AzureConfigure) ReadSPCredentials(fileSuffix string) error { + data, err := os.ReadFile(util.GetHomePath(AzureHomeDir, AzurePrefix+fileSuffix+".json")) + if err != nil { + return err + } + + return json.Unmarshal(data, &ac) +} + +func (ac *AzureConfigure) WriteSPCredentials(fileSuffix string) error { + data, err := json.MarshalIndent(ac, "", " ") + if err != nil { + return err + } + + data = []byte(string(data) + "\n") + + if err := os.MkdirAll(util.GetHomePath(AzureHomeDir), 0755); err != nil { + return err + } + + return os.WriteFile( + util.GetHomePath(AzureHomeDir, AzurePrefix+fileSuffix+".json"), + data, 0644) +} + +func (ac *AzureConfigure) NewAzureClient(ctx context.Context, fileName string) error { + if err := ac.ReadSPCredentials(fileName); err != nil { + return err + } + + cred, err := azidentity.NewClientSecretCredential(ac.TenantID, ac.ClientID, ac.ClientSecret, nil) + if err != nil { + return err + } + + GroupFactory, err := armresources.NewClientFactory(ac.SubscriptionID, cred, nil) + if err != nil { + return err + } + + ManagedClustersFactory, err := armcontainerservice.NewClientFactory(ac.SubscriptionID, cred, nil) + if err != nil { + return err + } + + VaultFactory, err := armkeyvault.NewClientFactory(ac.SubscriptionID, cred, nil) + if err != nil { + return err + } + + ac.Ctx = ctx + ac.Credentials = cred + ac.GroupClient = GroupFactory.NewResourceGroupsClient() + ac.VaultsClient = VaultFactory.NewVaultsClient() + ac.ManagedClustersClient = ManagedClustersFactory.NewManagedClustersClient() + + return nil +} + +func (ac *AzureConfigure) GetAzureClusterContext(groupName, clusterName string) ([]byte, error) { + var cpTitle = strings.ToUpper(AzureClusterProvider[:1]) + strings.ToLower(AzureClusterProvider[1:]) + + credentials, err := ac.ManagedClustersClient.ListClusterAdminCredentials(ac.Ctx, groupName, clusterName, nil) + if err != nil { + return nil, fmt.Errorf("kubecontext for %s provider's %s cluster not found", + cpTitle, clusterName) + } + + if len(credentials.CredentialResults.Kubeconfigs) == 1 { + return credentials.CredentialResults.Kubeconfigs[0].Value, nil + } + + return nil, fmt.Errorf("kubecontext for %s provider's %s cluster not found", + cpTitle, clusterName) +} + +func (ac *AzureConfigure) createKeyVaultResourceGroup(tenant string) error { + params := armresources.ResourceGroup{ + Location: to.Ptr(ac.Location), + } + + update, err := ac.GroupClient.CreateOrUpdate(ac.Ctx, tenant+"-"+util.SopsRootName, params, nil) + if err != nil { + var respErr *azcore.ResponseError + if errors.As(err, &respErr) && respErr.StatusCode == 403 { + zap.S().Warnf("permission denied to create Azure Resource Group: %s", tenant+"-"+util.SopsRootName) + return nil + } + + return err + } + + ac.ResourceGroupName = *update.Name + + zap.S().Infof("created Azure Resource Group: %s", ac.ResourceGroupName) + + return nil +} + +func (ac *AzureConfigure) existsKeyVaultResourceGroup(tenant string) (bool, error) { + existence, err := ac.GroupClient.CheckExistence(ac.Ctx, tenant+"-"+util.SopsRootName, nil) + if err != nil { + var respErr *azcore.ResponseError + if errors.As(err, &respErr) && respErr.StatusCode == 403 { + zap.S().Warnf("permission denied to check existence of Azure Resource Group: %s", + tenant+"-"+util.SopsRootName) + return false, nil + } + + return false, err + } + + return existence.Success, nil +} + +func (ac *AzureConfigure) CreateAzureKeyVault(tenant string) error { + if err := ac.createKeyVaultResourceGroup(tenant); err != nil { + return err + } + + if len(ac.ResourceGroupName) == 0 { + ac.ResourceGroupName = tenant + "-" + util.SopsRootName + } + + ac.KeyVaultName = generateKeyVaultName(tenant) + + params := armkeyvault.VaultCreateOrUpdateParameters{ + Location: to.Ptr(ac.Location), + Properties: &armkeyvault.VaultProperties{ + EnableRbacAuthorization: to.Ptr(true), + SKU: &armkeyvault.SKU{ + Family: to.Ptr(armkeyvault.SKUFamilyA), + Name: to.Ptr(armkeyvault.SKUNameStandard), + }, + TenantID: to.Ptr(ac.TenantID), + }, + } + + update, err := ac.VaultsClient.BeginCreateOrUpdate(ac.Ctx, ac.ResourceGroupName, ac.KeyVaultName, params, nil) + if err != nil { + var respErr *azcore.ResponseError + if errors.As(err, &respErr) && respErr.StatusCode == 403 { + zap.S().Warnf("permission denied to create Azure Key Vault: %s", + ac.KeyVaultName) + return nil + } + + return err + } + + result, err := update.PollUntilDone(ac.Ctx, nil) + if err != nil { + return err + } + + ac.KeyVaultURI = *result.Properties.VaultURI + + zap.S().Infof("created Azure Key Vault: %s, %s", ac.KeyVaultName, ac.KeyVaultURI) + + return nil +} + +func (ac *AzureConfigure) GetAzureKeyVault(tenant string) (bool, error) { + if ok, err := ac.existsKeyVaultResourceGroup(tenant); err != nil { + return false, err + } else if !ok { + return false, nil + } + + ac.KeyVaultName = generateKeyVaultName(tenant) + ac.ResourceGroupName = tenant + "-" + util.SopsRootName + + resp, err := ac.VaultsClient.Get(ac.Ctx, tenant+"-"+util.SopsRootName, ac.KeyVaultName, nil) + if err != nil { + var respErr *azcore.ResponseError + if errors.As(err, &respErr) && respErr.StatusCode == 404 { + return false, nil + } + + if errors.As(err, &respErr) && respErr.StatusCode == 403 { + zap.S().Warnf("permission denied to get Azure Key Vault: %s", ac.KeyVaultName) + return false, nil + } + + return false, err + } + + ac.KeyVaultURI = *resp.Properties.VaultURI + + return true, nil +} + +func (ac *AzureConfigure) GetAzureSecrets() (map[string][]byte, error) { + var secrets = make(map[string][]byte) + + client, err := azsecrets.NewClient(ac.KeyVaultURI, ac.Credentials, nil) + if err != nil { + return nil, err + } + + listSecrets := client.NewListSecretsPager(nil) + if err != nil { + return nil, err + } + + for listSecrets.More() { + page, err := listSecrets.NextPage(ac.Ctx) + if err != nil { + var respErr *azcore.ResponseError + if errors.As(err, &respErr) && respErr.StatusCode == 403 { + zap.S().Warnf("permission denied to list Azure Key Vault secrets") + return nil, nil + } + + return nil, err + } + + for _, val := range page.Value { + name := val.ID.Name() + version := val.ID.Version() + + secret, err := client.GetSecret(ac.Ctx, name, version, nil) + if err != nil { + var respErr *azcore.ResponseError + if errors.As(err, &respErr) && respErr.StatusCode == 403 { + zap.S().Warnf("permission denied to get of Azure Key Vault secret: %s", name) + return nil, nil + } + + return nil, err + } + + secrets[secret.ID.Name()] = []byte(*secret.Value) + } + } + + return secrets, nil +} + +func (ac *AzureConfigure) SetAzureSecret(keyName, value string) error { + client, err := azsecrets.NewClient(ac.KeyVaultURI, ac.Credentials, nil) + if err != nil { + return err + } + + params := azsecrets.SetSecretParameters{ + Value: to.Ptr(value), + } + + secret, err := client.SetSecret(ac.Ctx, keyName, params, nil) + if err != nil { + var respErr *azcore.ResponseError + if errors.As(err, &respErr) && respErr.StatusCode == 403 { + zap.S().Warnf("permission denied to create Azure Key Vault secret: %s", keyName) + return nil + } + + return err + } + + zap.S().Infof("created Azure Key Vault secret: %s, %s", keyName, *secret.ID) + + return nil +} diff --git a/providers/google_provider/gcp.go b/providers/google_provider/gcp.go new file mode 100644 index 0000000..28c500a --- /dev/null +++ b/providers/google_provider/gcp.go @@ -0,0 +1,373 @@ +package google_provider + +import ( + "context" + "encoding/base64" + "errors" + "fmt" + "hash/crc32" + "os" + "path/filepath" + "strings" + + "cloud.google.com/go/auth" + "cloud.google.com/go/auth/credentials" + secretmanager "cloud.google.com/go/secretmanager/apiv1" + "cloud.google.com/go/secretmanager/apiv1/secretmanagerpb" + "github.com/googleapis/gax-go/v2/apierror" + "go.uber.org/zap" + "google.golang.org/api/compute/v1" + container "google.golang.org/api/container/v1beta1" + "google.golang.org/api/googleapi" + "google.golang.org/api/iterator" + "google.golang.org/api/option" + "k8s.io/client-go/tools/clientcmd" + "k8s.io/client-go/tools/clientcmd/api" + + "rmk/util" +) + +const ( + GoogleClusterProvider = "gcp" + GoogleCredentialsPrefix = "gcp-credentials-" + GoogleHomeDir = ".config/gcloud" + + // List of APIs errors + apiErrorAlreadyExists = "alreadyExists" + apiErrorNotFound = "notFound" + gRPCErrorAlreadyExists = "AlreadyExists" + gRPCErrorPermissionDenied = "PermissionDenied" +) + +type GCPConfigure struct { + AppCredentials *auth.Credentials `yaml:"-"` + AppCredentialsPath string `yaml:"app-credentials-path"` + Ctx context.Context `yaml:"-"` + ProjectID string `yaml:"project-id"` +} + +func NewGCPConfigure(ctx context.Context, appCredentialsPath string) *GCPConfigure { + return &GCPConfigure{Ctx: ctx, AppCredentialsPath: appCredentialsPath} +} + +func (gcp *GCPConfigure) ReadSACredentials() error { + if !util.IsExists(gcp.AppCredentialsPath, true) { + return fmt.Errorf("google application credentials JSON file for GCP not found") + } + + data, err := os.ReadFile(gcp.AppCredentialsPath) + if err != nil { + return err + } + + gcp.AppCredentials, err = credentials.DetectDefault(&credentials.DetectOptions{CredentialsJSON: data}) + if err != nil { + return err + } + + gcp.ProjectID, err = gcp.AppCredentials.ProjectID(gcp.Ctx) + if err != nil { + return err + } + + return nil +} + +func (gcp *GCPConfigure) CopySACredentials(fileSuffix string) error { + if err := os.MkdirAll(util.GetHomePath(GoogleHomeDir), 0755); err != nil { + return err + } + + gcp.AppCredentialsPath = util.GetHomePath(GoogleHomeDir, GoogleCredentialsPrefix+fileSuffix+".json") + + return os.WriteFile(util.GetHomePath(GoogleHomeDir, GoogleCredentialsPrefix+fileSuffix+".json"), + gcp.AppCredentials.JSON(), 0644) +} + +func (gcp *GCPConfigure) GetGCPSecrets(tenant string) (map[string][]byte, error) { + var secrets = make(map[string][]byte) + + if err := gcp.ReadSACredentials(); err != nil { + return nil, err + } + + client, err := secretmanager.NewClient(gcp.Ctx, option.WithCredentialsJSON(gcp.AppCredentials.JSON())) + if err != nil { + return nil, err + } + + defer client.Close() + + listReq := &secretmanagerpb.ListSecretsRequest{ + Parent: fmt.Sprintf("projects/%s", gcp.ProjectID), + Filter: "labels.resource-group=" + tenant + "-" + util.SopsRootName, + } + + it := client.ListSecrets(gcp.Ctx, listReq) + for { + resp, err := it.Next() + if errors.Is(err, iterator.Done) { + break + } + + if err != nil { + var respError *apierror.APIError + if errors.As(err, &respError) && respError.GRPCStatus().Code().String() == gRPCErrorPermissionDenied { + zap.S().Warnf("permission denied to list GCP Secrets Manager secrets") + return nil, nil + } else { + return nil, err + } + } + + accReq := &secretmanagerpb.AccessSecretVersionRequest{Name: resp.Name + "/versions/latest"} + result, err := client.AccessSecretVersion(gcp.Ctx, accReq) + if err != nil { + var respError *apierror.APIError + if errors.As(err, &respError) && respError.GRPCStatus().Code().String() == gRPCErrorPermissionDenied { + zap.S().Warnf("permission denied to get access to GCP Secrets Manager secrets values") + return nil, nil + } else { + return nil, err + } + } + + crc32c := crc32.MakeTable(crc32.Castagnoli) + checksum := int64(crc32.Checksum(result.Payload.Data, crc32c)) + if checksum != *result.Payload.DataCrc32C { + return nil, fmt.Errorf("data corruption detected for GCP Secrets Manager secrets value: %s", resp.Name) + } + + secrets[filepath.Base(resp.Name)] = result.Payload.Data + } + + return secrets, nil +} + +func (gcp *GCPConfigure) SetGCPSecret(tenant, region, keyName string, value []byte) error { + if err := gcp.ReadSACredentials(); err != nil { + return err + } + + client, err := secretmanager.NewClient(gcp.Ctx, option.WithCredentialsJSON(gcp.AppCredentials.JSON())) + if err != nil { + return err + } + + defer client.Close() + + secretReq := &secretmanagerpb.CreateSecretRequest{ + Parent: fmt.Sprintf("projects/%s", gcp.ProjectID), + SecretId: keyName, + Secret: &secretmanagerpb.Secret{ + Replication: &secretmanagerpb.Replication{ + Replication: &secretmanagerpb.Replication_UserManaged_{ + UserManaged: &secretmanagerpb.Replication_UserManaged{ + Replicas: []*secretmanagerpb.Replication_UserManaged_Replica{{Location: region}}, + }, + }, + }, + Labels: map[string]string{"resource-group": tenant + "-" + util.SopsRootName}, + }, + } + + _, err = client.CreateSecret(gcp.Ctx, secretReq) + if err != nil { + var respError *apierror.APIError + if errors.As(err, &respError) && respError.GRPCStatus().Code().String() == gRPCErrorPermissionDenied { + zap.S().Warnf("permission denied to create GCP Secrets Manager secret: %s", keyName) + } else if respError.GRPCStatus().Code().String() != gRPCErrorAlreadyExists { + return err + } + } + + secretVersionReq := &secretmanagerpb.AddSecretVersionRequest{ + Parent: fmt.Sprintf("projects/%s/secrets/%s", gcp.ProjectID, keyName), + Payload: &secretmanagerpb.SecretPayload{Data: value}, + } + + version, err := client.AddSecretVersion(gcp.Ctx, secretVersionReq) + if err != nil { + var respError *apierror.APIError + if errors.As(err, &respError) && respError.GRPCStatus().Code().String() == gRPCErrorPermissionDenied { + zap.S().Warnf("permission denied to add GCP Secrets Manager secret %s value", keyName) + } else { + return err + } + } + + zap.S().Infof("created GCP Secrets Manager secret: %s, %s", keyName, version.Name) + + return nil +} + +func (gcp *GCPConfigure) CreateGCPCloudNATGateway(region string) error { + if err := gcp.ReadSACredentials(); err != nil { + return err + } + + client, err := compute.NewService(gcp.Ctx, option.WithCredentialsJSON(gcp.AppCredentials.JSON())) + if err != nil { + return err + } + + routerNat := &compute.RouterNat{ + AutoNetworkTier: "STANDARD", + EndpointTypes: []string{"ENDPOINT_TYPE_VM"}, + Name: "default-nat-" + region, + NatIpAllocateOption: "AUTO_ONLY", + SourceSubnetworkIpRangesToNat: "ALL_SUBNETWORKS_ALL_IP_RANGES", + Type: "PUBLIC", + } + + router := &compute.Router{ + Name: "default-router-" + region, + Nats: []*compute.RouterNat{routerNat}, + Network: "projects/" + gcp.ProjectID + "/global/networks/default", + } + + _, err = client.Routers.Insert(gcp.ProjectID, region, router).Context(gcp.Ctx).Do() + if err != nil { + var respError *googleapi.Error + if errors.As(err, &respError) && respError.Code == 409 && respError.Errors[0].Reason == apiErrorAlreadyExists { + zap.S().Infof("GCP router %s with router NAT %s already exists for region %s", + router.Name, routerNat.Name, region) + return nil + } + + return err + } + + zap.S().Infof("created GCP router %s with router NAT %s", router.Name, routerNat.Name) + + return nil +} + +func (gcp *GCPConfigure) DeleteGCPCloudNATGateway(region string) error { + if err := gcp.ReadSACredentials(); err != nil { + return err + } + + client, err := compute.NewService(gcp.Ctx, option.WithCredentialsJSON(gcp.AppCredentials.JSON())) + if err != nil { + return err + } + + _, err = client.Routers.Delete(gcp.ProjectID, region, "default-router-"+region).Context(gcp.Ctx).Do() + if err != nil { + var respError *googleapi.Error + if errors.As(err, &respError) && respError.Code == 404 && respError.Errors[0].Reason == apiErrorNotFound { + return nil + } + + return err + } + + zap.S().Infof("deleted GCP router %s for region %s", "default-router-"+region, region) + + return nil +} + +func (gcp *GCPConfigure) GetGCPClusterContext(clusterName string) ([]byte, error) { + var cluster *container.Cluster + + if err := gcp.ReadSACredentials(); err != nil { + return nil, err + } + + client, err := container.NewService(gcp.Ctx, option.WithCredentialsJSON(gcp.AppCredentials.JSON())) + if err != nil { + return nil, err + } + + resp, err := client.Projects.Zones.Clusters.List(gcp.ProjectID, "-").Context(gcp.Ctx).Do() + if err != nil { + return nil, err + } + + for _, val := range resp.Clusters { + if val.Name == clusterName { + cluster = val + break + } + } + + if cluster != nil { + return gcp.generateUserKubeconfig(cluster) + } + + return nil, fmt.Errorf("kubecontext for %s provider's %s cluster not found", + strings.ToUpper(GoogleClusterProvider), clusterName) +} + +func (gcp *GCPConfigure) generateUserKubeconfig(cluster *container.Cluster) ([]byte, error) { + var execEnvVars []api.ExecEnvVar + + userName := gcp.getKubeConfigUserName(cluster.Name) + cfg, err := gcp.generateBaseKubeConfig(cluster) + if err != nil { + return nil, fmt.Errorf("creating base kubeconfig: %w", err) + } + + execEnvVars = append(execEnvVars, + api.ExecEnvVar{Name: "GOOGLE_APPLICATION_CREDENTIALS", Value: gcp.AppCredentialsPath}, + ) + + // Version v1alpha1 was removed in Kubernetes v1.23. + // Version v1 was released in Kubernetes v1.23. + // Version v1beta1 was selected as it has the widest range of support + // This should be changed to v1 once EKS no longer supports Kubernetes ~/.local/bin/rmk-completion-zsh.sh -chmod +x ~/.local/bin/rmk-completion-zsh.sh -echo "PROG=rmk source ~/.local/bin/rmk-completion-zsh.sh" >> ~/.zshrc` - - // CompletionZshScript https://github.com/urfave/cli/blob/v2.27.1/autocomplete/zsh_autocomplete - CompletionZshScript = `#compdef $PROG - -_cli_zsh_autocomplete() { - local -a opts - local cur - cur=${words[-1]} - if [[ "$cur" == "-"* ]]; then - opts=("${(@f)$(${words[@]:0:#words[@]-1} ${cur} --generate-bash-completion)}") - else - opts=("${(@f)$(${words[@]:0:#words[@]-1} --generate-bash-completion)}") - fi - - if [[ "${opts[1]}" != "" ]]; then - _describe 'values' opts - else - _files - fi -} - -compdef _cli_zsh_autocomplete $PROG` -) diff --git a/util/cli.go b/util/cli.go new file mode 100644 index 0000000..cbb55d5 --- /dev/null +++ b/util/cli.go @@ -0,0 +1,84 @@ +package util + +import ( + "fmt" + "io" + "os" + "strings" + "unicode/utf8" + + "github.com/urfave/cli/v2" + "github.com/urfave/cli/v2/altsrc" +) + +func cliArgContains(flagName string) bool { + for _, name := range strings.Split(flagName, ",") { + name = strings.TrimSpace(name) + count := utf8.RuneCountInString(name) + if count > 2 { + count = 2 + } + + flag := fmt.Sprintf("%s%s", strings.Repeat("-", count), name) + + for _, a := range os.Args { + if a == flag { + return true + } + } + } + + return false +} + +func printFlagSuggestions(lastArg string, flags []cli.Flag, writer io.Writer) { + cur := strings.TrimPrefix(lastArg, "-") + cur = strings.TrimPrefix(cur, "-") + for _, flag := range flags { + // skip hidden flags for bool type + if boolFlag, ok := flag.(*cli.BoolFlag); ok && boolFlag.Hidden { + continue + } + // skip hidden flags for altsrc bool type + if altsrcBoolFlag, ok := flag.(*altsrc.BoolFlag); ok && altsrcBoolFlag.Hidden { + continue + } + // skip hidden flags for string type + if stringFlag, ok := flag.(*cli.StringFlag); ok && stringFlag.Hidden { + continue + } + // skip hidden flags for altsrc string type + if altsrcStringFlag, ok := flag.(*altsrc.StringFlag); ok && altsrcStringFlag.Hidden { + continue + } + + for _, name := range flag.Names() { + name = strings.TrimSpace(name) + // this will get total count utf8 letters in flag name + count := utf8.RuneCountInString(name) + if count > 2 { + count = 2 // reuse this count to generate single - or -- in flag completion + } + // if flag name has more than one utf8 letter and last argument in cli has -- prefix then + // skip flag completion for short flags example -v or -x + if strings.HasPrefix(lastArg, "--") && count == 1 { + continue + } + // match if last argument matches this flag and it is not repeated + if strings.HasPrefix(name, cur) && cur != name && !cliArgContains(name) { + flagCompletion := fmt.Sprintf("%s%s", strings.Repeat("-", count), name) + _, _ = fmt.Fprintln(writer, flagCompletion) + } + } + } +} + +func ShellCompleteCustomOutput(c *cli.Context) { + if len(os.Args) > 2 { + if os.Args[len(os.Args)-2] != "" && strings.HasPrefix(os.Args[len(os.Args)-2], "-") { + printFlagSuggestions(os.Args[len(os.Args)-2], c.Command.Flags, c.App.Writer) + + return + } + } +} diff --git a/util/dictionary.go b/util/dictionary.go new file mode 100644 index 0000000..c923071 --- /dev/null +++ b/util/dictionary.go @@ -0,0 +1,75 @@ +package util + +const ( + CAPI = "capi" + CAPIContextName = K3DPrefix + "-" + CAPI + GitSSHPrivateKey = ".ssh/id_rsa" + GlobalsFileName = "globals.yaml.gotmpl" + HelmfileFileName = "helmfile.yaml" + HelmfileGoTmplName = HelmfileFileName + ".gotmpl" + HelpFlagFull = "--help" + K3DPrefix = "k3d" + LocalClusterProvider = K3DPrefix + RMKBin = "rmk" + RMKBucketName = "edenlabllc-rmk" + RMKBucketRegion = "eu-north-1" + RMKConfig = "config" + RMKDir = ".rmk" + RMKSymLinkPath = "/usr/local/bin/rmk" + RMKToolsDir = "tools" + ReadmeFileName = "README.md" + RegionException = "us-east-1" + ReleasesFileName = "releases.yaml" + SecretSpecFile = ".spec.yaml.gotmpl" + SopsAgeKeyExt = ".txt" + SopsAgeKeyFile = ".keys.txt" + SopsRootName = "sops-age-keys" + SopsConfigFile = ".sops.yaml" + TenantProjectCodeOwners = "docs/CODEOWNERS" + TenantProjectDIR = ".PROJECT" + TenantProjectFile = "project.yaml" + TenantProjectGitIgn = ".gitignore" + TenantValuesDIR = "etc" + ToolsBinDir = "bin" + ToolsTmpDir = "tmp" + ToolsVersionDir = "version" + + ConfigNotInitializedErrorText = "RMK config not initialized, " + + "please run command 'rmk config init' with specific parameters" + // UnknownErrorText standard text for unknown errors + UnknownErrorText = "unknown error when calling %s" + //HelmPluginExist HelmSecretsIsNotEncrypted HelmSecretsAlreadyEncrypted - exception err text matching + HelmPluginExist = "Error: plugin already exists" + HelmSecretsIsNotEncrypted = "File is not encrypted: " + HelmSecretsAlreadyEncrypted = "Already encrypted: " + HelmSecretsOutputPrefix = "[helm-secrets] " + HelmSecretsError = "Error: plugin \"secrets\" exited with error" + + CompletionZshDescription = `Run the following scripts to enable Zsh completion: + +rmk completion zsh > ~/.local/bin/rmk-completion-zsh.sh +chmod +x ~/.local/bin/rmk-completion-zsh.sh +echo "PROG=rmk source ~/.local/bin/rmk-completion-zsh.sh" >> ~/.zshrc` + + // CompletionZshScript https://github.com/urfave/cli/blob/v2.27.1/autocomplete/zsh_autocomplete + CompletionZshScript = `#compdef $PROG + +_cli_zsh_autocomplete() { + local -a opts + local cur + cur=${words[-1]} + if [[ "$cur" == "-"* ]]; then + opts=("${(@f)$(${words[@]:0:#words[@]-1} ${cur} --generate-bash-completion)}") + else + opts=("${(@f)$(${words[@]:0:#words[@]-1} --generate-bash-completion)}") + fi + + if [[ "${opts[1]}" != "" ]]; then + _describe 'values' opts + else + _files + fi +} + +compdef _cli_zsh_autocomplete $PROG` +) diff --git a/go_getter/getter.go b/util/getter.go similarity index 99% rename from go_getter/getter.go rename to util/getter.go index 3e90261..b53f013 100644 --- a/go_getter/getter.go +++ b/util/getter.go @@ -1,4 +1,4 @@ -package go_getter +package util import ( "context" diff --git a/system/system.go b/util/system.go similarity index 73% rename from system/system.go rename to util/system.go index e91f73f..ff5b914 100644 --- a/system/system.go +++ b/util/system.go @@ -1,4 +1,4 @@ -package system +package util import ( "archive/tar" @@ -14,10 +14,7 @@ import ( "regexp" "strings" "sync" - "unicode/utf8" - "github.com/urfave/cli/v2" - "github.com/urfave/cli/v2/altsrc" "go.uber.org/zap" ) @@ -35,7 +32,7 @@ type SpecCMD struct { SensKeyWords []string } -func (s *SpecCMD) AddEnv() error { +func (s *SpecCMD) AddOSEnv() error { path, exists := os.LookupEnv("PATH") if exists { if err := os.Setenv("PATH", GetHomePath(".local", "bin")+":"+path); err != nil { @@ -401,94 +398,17 @@ func UnTar(dst, excludeRegexp string, r io.Reader) error { } } -func cliArgContains(flagName string) bool { - for _, name := range strings.Split(flagName, ",") { - name = strings.TrimSpace(name) - count := utf8.RuneCountInString(name) - if count > 2 { - count = 2 - } - - flag := fmt.Sprintf("%s%s", strings.Repeat("-", count), name) - - for _, a := range os.Args { - if a == flag { - return true - } - } - } - - return false -} - -func printFlagSuggestions(lastArg string, flags []cli.Flag, writer io.Writer) { - cur := strings.TrimPrefix(lastArg, "-") - cur = strings.TrimPrefix(cur, "-") - for _, flag := range flags { - // skip hidden flags for bool type - if boolFlag, ok := flag.(*cli.BoolFlag); ok && boolFlag.Hidden { - continue - } - // skip hidden flags for altsrc bool type - if altsrcBoolFlag, ok := flag.(*altsrc.BoolFlag); ok && altsrcBoolFlag.Hidden { - continue - } - // skip hidden flags for string type - if stringFlag, ok := flag.(*cli.StringFlag); ok && stringFlag.Hidden { - continue - } - // skip hidden flags for altsrc string type - if altsrcStringFlag, ok := flag.(*altsrc.StringFlag); ok && altsrcStringFlag.Hidden { - continue - } - - for _, name := range flag.Names() { - name = strings.TrimSpace(name) - // this will get total count utf8 letters in flag name - count := utf8.RuneCountInString(name) - if count > 2 { - count = 2 // reuse this count to generate single - or -- in flag completion - } - // if flag name has more than one utf8 letter and last argument in cli has -- prefix then - // skip flag completion for short flags example -v or -x - if strings.HasPrefix(lastArg, "--") && count == 1 { - continue - } - // match if last argument matches this flag and it is not repeated - if strings.HasPrefix(name, cur) && cur != name && !cliArgContains(name) { - flagCompletion := fmt.Sprintf("%s%s", strings.Repeat("-", count), name) - _, _ = fmt.Fprintln(writer, flagCompletion) - } - } - } -} - -func ShellCompleteCustomOutput(c *cli.Context) { - if len(os.Args) > 2 { - if os.Args[len(os.Args)-2] != "" && strings.HasPrefix(os.Args[len(os.Args)-2], "-") { - printFlagSuggestions(os.Args[len(os.Args)-2], c.Command.Flags, c.App.Writer) - - return - } +func CreateTempYAMLFile(dirPath, fileName string, content []byte) (string, error) { + file, err := os.CreateTemp(dirPath, fileName+".*.yaml") + if err != nil { + return "", err } -} -func ValidateArtifactModeDefault(c *cli.Context, errorMsg string) error { - if c.String("artifact-mode") == ArtifactModeDefault && !c.IsSet("github-token") { - if errorMsg == "" { - return fmt.Errorf(ConfigNotInitializedErrorText) - } else { - return fmt.Errorf(errorMsg) - } + if _, err := file.Write(content); err != nil { + return "", err } - return nil -} - -func ValidateNArg(c *cli.Context, expectedNArg int) error { - if c.NArg() != expectedNArg { - return fmt.Errorf("exactly %d argument(s) required for '%s' command", expectedNArg, c.Command.Name) - } + defer file.Close() - return nil + return file.Name(), nil } diff --git a/util/validate.go b/util/validate.go new file mode 100644 index 0000000..b9cf119 --- /dev/null +++ b/util/validate.go @@ -0,0 +1,15 @@ +package util + +import ( + "fmt" + + "github.com/urfave/cli/v2" +) + +func ValidateNArg(c *cli.Context, expectedNArg int) error { + if c.NArg() != expectedNArg { + return fmt.Errorf("exactly %d argument(s) required for '%s' command", expectedNArg, c.Command.Name) + } + + return nil +}