From 6960aba6be076a0c64223b9e5c65cf7ed1cd8481 Mon Sep 17 00:00:00 2001 From: xh3b4sd Date: Thu, 11 Sep 2025 17:06:47 +0200 Subject: [PATCH 01/13] inject preview deployments into nested stacks --- go.mod | 1 + go.sum | 2 + log.json | 36 ++ pkg/operator/infrastructure/ensure.go | 2 + .../infrastructure/preview/preview.go | 30 ++ pkg/operator/infrastructure/preview/render.go | 69 +++ .../infrastructure/preview/render_test.go | 64 +++ .../infrastructure/preview/resource.go | 9 + .../preview/testdata/000/inp.yaml.golden | 308 +++++++++++++ .../preview/testdata/000/out.yaml.golden | 413 ++++++++++++++++++ pkg/scanner/scanner.go | 26 ++ pkg/scanner/search.go | 65 +++ pkg/scanner/search_test.go | 71 +++ pkg/scanner/testdata/000/inp.yaml.golden | 15 + pkg/scanner/testdata/000/out.yaml.golden | 2 + pkg/scanner/testdata/001/inp.yaml.golden | 23 + pkg/scanner/testdata/001/out.yaml.golden | 3 + pkg/scanner/testdata/002/inp.yaml.golden | 15 + pkg/scanner/testdata/002/out.yaml.golden | 9 + pkg/scanner/testdata/003/inp.yaml.golden | 32 ++ pkg/scanner/testdata/003/out.yaml.golden | 3 + 21 files changed, 1198 insertions(+) create mode 100644 log.json create mode 100644 pkg/operator/infrastructure/preview/preview.go create mode 100644 pkg/operator/infrastructure/preview/render.go create mode 100644 pkg/operator/infrastructure/preview/render_test.go create mode 100644 pkg/operator/infrastructure/preview/resource.go create mode 100644 pkg/operator/infrastructure/preview/testdata/000/inp.yaml.golden create mode 100644 pkg/operator/infrastructure/preview/testdata/000/out.yaml.golden create mode 100644 pkg/scanner/scanner.go create mode 100644 pkg/scanner/search.go create mode 100644 pkg/scanner/search_test.go create mode 100644 pkg/scanner/testdata/000/inp.yaml.golden create mode 100644 pkg/scanner/testdata/000/out.yaml.golden create mode 100644 pkg/scanner/testdata/001/inp.yaml.golden create mode 100644 pkg/scanner/testdata/001/out.yaml.golden create mode 100644 pkg/scanner/testdata/002/inp.yaml.golden create mode 100644 pkg/scanner/testdata/002/out.yaml.golden create mode 100644 pkg/scanner/testdata/003/inp.yaml.golden create mode 100644 pkg/scanner/testdata/003/out.yaml.golden diff --git a/go.mod b/go.mod index 205016d..5708073 100644 --- a/go.mod +++ b/go.mod @@ -62,6 +62,7 @@ require ( github.com/google/go-querystring v1.1.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 // indirect + github.com/iancoleman/strcase v0.3.0 github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect diff --git a/go.sum b/go.sum index 69c5f05..54b1337 100644 --- a/go.sum +++ b/go.sum @@ -90,6 +90,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 h1:cLN4IBkmkYZNnk7EAJ0BHIethd+J6LqxFNw5mSiI2bM= github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk= +github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= +github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= diff --git a/log.json b/log.json new file mode 100644 index 0000000..925e683 --- /dev/null +++ b/log.json @@ -0,0 +1,36 @@ +[ + { + "time": "2025-09-01 14:47:24", + "level": "warning", + "message": "git ref unresolvable", + "branch": "lite", + "owner": "0xSplits", + "reason": "branch not found", + "repository": "infrastructure", + "suggestion": "this issue might be caused by a user error or eventual consistency of the underlying backend", + "caller": "/build/pkg/operator/reference/github.go:42" + }, + { + "time": "2025-09-01 14:44:06", + "level": "error", + "message": "worker execution failed", + "stack": { + "context": [ + { + "key": "handler", + "value": "policy" + } + ], + "description": "This critical error indicates that some cached state of the current reconciliation loop was missing, which means that the operator does not know how to proceed safely.", + "trace": [ + "/build/pkg/operator/policy/ensure.go:42", + "/build/pkg/operator/policy/ensure.go:12", + "/go/pkg/mod/github.com/0x!splits/workit@v0.5.0/handler/metrics/ensure.go:44", + "/go/pkg/mod/github.com/0x!splits/workit@v0.5.0/worker/sequence/ensure.go:79", + "/go/pkg/mod/github.com/0x!splits/workit@v0.5.0/worker/sequence/ensure.go:24", + "/go/pkg/mod/github.com/0x!splits/workit@v0.5.0/worker/sequence/daemon.go:23" + ] + }, + "caller": "/go/pkg/mod/github.com/0x!splits/workit@v0.5.0/worker/sequence/daemon.go:39" + } +] diff --git a/pkg/operator/infrastructure/ensure.go b/pkg/operator/infrastructure/ensure.go index 28f2dc4..04cc447 100644 --- a/pkg/operator/infrastructure/ensure.go +++ b/pkg/operator/infrastructure/ensure.go @@ -68,6 +68,8 @@ func (i *Infrastructure) Ensure() error { } } + // TODO somewhere here we need to inject the preview resources + { err = i.putObj(pat, byt) if err != nil { diff --git a/pkg/operator/infrastructure/preview/preview.go b/pkg/operator/infrastructure/preview/preview.go new file mode 100644 index 0000000..7a42c73 --- /dev/null +++ b/pkg/operator/infrastructure/preview/preview.go @@ -0,0 +1,30 @@ +package preview + +import ( + "fmt" + + "github.com/0xSplits/kayron/pkg/scanner" + "github.com/xh3b4sd/tracer" +) + +type Config struct { + Inp []byte +} + +type Preview struct { + inp []byte + sca *scanner.Scanner +} + +func New(c Config) *Preview { + if c.Inp == nil { + tracer.Panic(tracer.Mask(fmt.Errorf("%T.Inp must not be empty", c))) + } + + return &Preview{ + inp: c.Inp, + sca: scanner.New(scanner.Config{ + Inp: c.Inp, + }), + } +} diff --git a/pkg/operator/infrastructure/preview/render.go b/pkg/operator/infrastructure/preview/render.go new file mode 100644 index 0000000..67927dd --- /dev/null +++ b/pkg/operator/infrastructure/preview/render.go @@ -0,0 +1,69 @@ +package preview + +import ( + "bytes" + "fmt" + + "github.com/iancoleman/strcase" +) + +func (p *Preview) Render(bra []string) []byte { + var res Resource + { + res = Resource{ + Ser: p.sca.Search([]byte(" Service:")), + Tas: p.sca.Search([]byte(" TaskDefinition:")), + Dom: p.sca.Search([]byte(" DomainRecord:")), + Tar: p.sca.Search([]byte(" TargetGroup:")), + Lis: p.sca.Search([]byte(" ListenerRule:")), + } + } + + var out []byte + { + out = p.inp + } + + for _, x := range bra { + out = append(out, p.render(res, x)...) + } + + return out +} + +func (p *Preview) render(res Resource, bra string) []byte { + var out []byte + + var cam string + var keb string + { + cam = strcase.ToCamel(bra) // FancyFeatureBranch + keb = strcase.ToKebab(bra) // fancy-feature-branch + } + + fmt.Printf("%#v\n", cam) + fmt.Printf("%#v\n", keb) + + { + res.Ser = bytes.Replace(res.Ser, []byte(" Service:"), fmt.Appendf(nil, " Service%s:", cam), 1) + res.Tas = bytes.Replace(res.Tas, []byte(" TaskDefinition:"), fmt.Appendf(nil, " TaskDefinition%s:", cam), 1) + res.Dom = bytes.Replace(res.Dom, []byte(" DomainRecord:"), fmt.Appendf(nil, " DomainRecord%s:", cam), 1) + res.Tar = bytes.Replace(res.Tar, []byte(" TargetGroup:"), fmt.Appendf(nil, " TargetGroup%s:", cam), 1) + res.Lis = bytes.Replace(res.Lis, []byte(" ListenerRule:"), fmt.Appendf(nil, " ListenerRule%s:", cam), 1) + } + + { + out = append(out, '\n') + out = append(out, res.Ser...) + out = append(out, '\n') + out = append(out, res.Tas...) + out = append(out, '\n') + out = append(out, res.Dom...) + out = append(out, '\n') + out = append(out, res.Tar...) + out = append(out, '\n') + out = append(out, res.Lis...) + } + + return out +} diff --git a/pkg/operator/infrastructure/preview/render_test.go b/pkg/operator/infrastructure/preview/render_test.go new file mode 100644 index 0000000..f9fc712 --- /dev/null +++ b/pkg/operator/infrastructure/preview/render_test.go @@ -0,0 +1,64 @@ +package preview + +import ( + "bytes" + "fmt" + "os" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func Test_Operator_Infrastructure_Preview_Render(t *testing.T) { + testCases := []struct { + bra []string + }{ + // Case 000 + { + bra: []string{ + "fancy-feature-branch", + }, + }, + } + + for i, tc := range testCases { + t.Run(fmt.Sprintf("%03d", i), func(t *testing.T) { + var err error + + var inp []byte + { + inp, err = os.ReadFile(fmt.Sprintf("./testdata/%03d/inp.yaml.golden", i)) + if err != nil { + t.Fatal("expected", nil, "got", err) + } + } + + var out []byte + { + out, err = os.ReadFile(fmt.Sprintf("./testdata/%03d/out.yaml.golden", i)) + if err != nil { + t.Fatal("expected", nil, "got", err) + } + } + + var pre *Preview + { + pre = New(Config{ + Inp: inp, + }) + } + + var res []byte + { + res = pre.Render(tc.bra) + if err != nil { + t.Fatal("expected", nil, "got", err) + } + } + + if dif := cmp.Diff(bytes.TrimSpace(out), bytes.TrimSpace(res)); dif != "" { + t.Fatalf("-expected +actual:\n%s", dif) + } + }) + } +} diff --git a/pkg/operator/infrastructure/preview/resource.go b/pkg/operator/infrastructure/preview/resource.go new file mode 100644 index 0000000..e47c473 --- /dev/null +++ b/pkg/operator/infrastructure/preview/resource.go @@ -0,0 +1,9 @@ +package preview + +type Resource struct { + Ser []byte + Tas []byte + Dom []byte + Tar []byte + Lis []byte +} diff --git a/pkg/operator/infrastructure/preview/testdata/000/inp.yaml.golden b/pkg/operator/infrastructure/preview/testdata/000/inp.yaml.golden new file mode 100644 index 0000000..6914ac6 --- /dev/null +++ b/pkg/operator/infrastructure/preview/testdata/000/inp.yaml.golden @@ -0,0 +1,308 @@ +AWSTemplateFormatVersion: "2010-09-09" +Description: "CloudFormation template for the Splits Lite containers and their load balancer configuration" + +Parameters: + Environment: + Description: "the name of the deployed environment" + Type: String + + DiscoveryStack: + Description: "the name of the discovery stack" + Type: String + + FargateStack: + Description: "the name of the server stack" + Type: String + + NetworkStack: + Description: "the name of the networking stack" + Type: String + + Secret: + Description: "the name of the environment/stack specific secret" + Type: String + + LiteCertificate: + Description: "the ARN of the SSL certificate for the Splits Lite containers" + Type: String + + LiteDomain: + Description: "the domain name of the Splits Lite DNS" + Type: String + + LitePort: + Description: "the server port of the Splits Lite containers" + Type: Number + + LiteVersion: + Description: "the Docker image tag for the Splits Lite containers" + Type: String + +Conditions: + IsProd: !Equals [!Ref Environment, production] + +Resources: + Cluster: + Type: AWS::ECS::Cluster + Properties: + ClusterName: !Ref AWS::StackName + Tags: + - Key: "environment" + Value: !Ref Environment + - Key: "service" + Value: "splits-lite" # must match repo name + + Service: + Type: AWS::ECS::Service + Properties: + Cluster: !Ref Cluster + ServiceName: !Sub "${AWS::StackName}-lite" + DesiredCount: 1 + LaunchType: "FARGATE" + TaskDefinition: !Ref TaskDefinition + NetworkConfiguration: + AwsvpcConfiguration: + AssignPublicIp: "DISABLED" + SecurityGroups: + - !Ref SecurityGroup + Subnets: + - Fn::ImportValue: !Sub "${NetworkStack}-PrivateSubnet1ID" + - Fn::ImportValue: !Sub "${NetworkStack}-PrivateSubnet2ID" + LoadBalancers: + - TargetGroupArn: !Ref TargetGroup + ContainerName: !Sub "${AWS::StackName}-lite" + ContainerPort: !Ref LitePort + ServiceRegistries: + - ContainerName: !Sub "${AWS::StackName}-lite" + RegistryArn: !GetAtt ServiceDiscovery.Arn + Tags: + - Key: "environment" + Value: !Ref Environment + - Key: "service" + Value: "splits-lite" # must match repo name + DependsOn: + - Cluster + + TaskDefinition: + Type: AWS::ECS::TaskDefinition + Properties: + Family: !Sub "${AWS::StackName}-lite" + RequiresCompatibilities: + - FARGATE + Cpu: 512 + Memory: 1024 + NetworkMode: awsvpc + ExecutionRoleArn: !GetAtt TaskExecutionRole.Arn + TaskRoleArn: !Sub "arn:aws:iam::${AWS::AccountId}:role/splits-ecs-role" + ContainerDefinitions: + - Name: !Sub "${AWS::StackName}-lite" + Image: !Sub "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/splits-lite:${LiteVersion}" + PortMappings: + - ContainerPort: !Ref LitePort + Protocol: "tcp" + Essential: true + Secrets: + - Name: ALCHEMY_API_KEY + ValueFrom: !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${Secret}:ALCHEMY_API_KEY::" + - Name: WALLETCONNECT_PROJECT_ID + ValueFrom: !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${Secret}:WALLETCONNECT_PROJECT_ID::" + LogConfiguration: + LogDriver: awslogs + Options: + awslogs-group: !Sub "/fargate/${AWS::StackName}/lite" + awslogs-region: !Ref AWS::Region + awslogs-stream-prefix: ecs + Tags: + - Key: "environment" + Value: !Ref Environment + + LogGroup: + Type: AWS::Logs::LogGroup + Properties: + LogGroupName: !Sub "/fargate/${AWS::StackName}/lite" + RetentionInDays: 7 + Tags: + - Key: "environment" + Value: !Ref Environment + + TaskExecutionRole: + Type: AWS::IAM::Role + Properties: + Path: "/" + AssumeRolePolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Action: sts:AssumeRole + Principal: + Service: ecs-tasks.amazonaws.com + ManagedPolicyArns: + - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy + Policies: + - PolicyName: secrets-access + PolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Action: + - secretsmanager:GetSecretValue + Resource: !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${Secret}*" + Tags: + - Key: "environment" + Value: !Ref Environment + + DomainRecord: + Type: AWS::Route53::RecordSet + Properties: + HostedZoneId: + Fn::ImportValue: !Sub "${NetworkStack}-EnvironmentHostedZoneId" + Name: !Ref LiteDomain + Type: A + AliasTarget: + DNSName: + Fn::ImportValue: !Sub "${FargateStack}-ApplicationLoadBalancerBaseDnsName" + HostedZoneId: + Fn::ImportValue: !Sub "${FargateStack}-ApplicationLoadBalancerCanonicalHostedZoneId" + + TargetGroup: + Type: AWS::ElasticLoadBalancingV2::TargetGroup + Properties: + Port: !Ref LitePort + Protocol: "HTTP" + VpcId: + Fn::ImportValue: !Sub "${NetworkStack}-VpcID" + TargetType: "ip" + HealthCheckPath: "/" + HealthCheckProtocol: "HTTP" + Matcher: + HttpCode: 200 + Tags: + - Key: "environment" + Value: !Ref Environment + + ListenerRule: + Type: AWS::ElasticLoadBalancingV2::ListenerRule + Properties: + Actions: + - Type: forward + TargetGroupArn: !Ref TargetGroup + Conditions: + - Field: host-header + HostHeaderConfig: + Values: + - !Ref LiteDomain + ListenerArn: + Fn::ImportValue: !Sub "${FargateStack}-ApplicationLoadBalancerListenerArn" + Priority: 12 # Host header = lite.{environment}.splits.org + + ListenerCertificate: + Type: AWS::ElasticLoadBalancingV2::ListenerCertificate + Properties: + Certificates: + - CertificateArn: !Ref LiteCertificate + ListenerArn: + Fn::ImportValue: !Sub "${FargateStack}-ApplicationLoadBalancerListenerArn" + + # ProdListenerRule registers the target group of the splits lite containers + # under the shorthand production domain lite.splits.org, but only for the + # production environment. Note that in order to make this work, we are also + # registering the respective SSL certificate in the load balancer, so that the + # TLS handshake may succeed. + ProdListenerRule: + Type: AWS::ElasticLoadBalancingV2::ListenerRule + Condition: IsProd + Properties: + Actions: + - Type: "forward" + TargetGroupArn: !Ref TargetGroup + Conditions: + - Field: "host-header" + HostHeaderConfig: + Values: + - "lite.splits.org" + ListenerArn: + Fn::ImportValue: !Sub "${FargateStack}-ApplicationLoadBalancerListenerArn" + Priority: 16 # Host header = lite.splits.org + + ProdListenerCertificate: + Condition: IsProd + Type: AWS::ElasticLoadBalancingV2::ListenerCertificate + Properties: + Certificates: + - CertificateArn: "arn:aws:acm:us-west-2:995626699990:certificate/4e1a3a6b-c119-4594-a6c9-7f667401d78e" + ListenerArn: + Fn::ImportValue: !Sub "${FargateStack}-ApplicationLoadBalancerListenerArn" + + SecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: "the security group for the Splits Lite containers in the Splits Lite stack" + VpcId: + Fn::ImportValue: !Sub "${NetworkStack}-VpcID" + Tags: + - Key: "environment" + Value: !Ref Environment + + FromAlloyToLiteIngress: + Type: AWS::EC2::SecurityGroupIngress + Properties: + Description: "the ingress rule to allow traffic from the Alloy to the Splits Lite containers" + GroupId: !Ref SecurityGroup + IpProtocol: "tcp" + FromPort: !Ref LitePort + ToPort: !Ref LitePort + SourceSecurityGroupId: + Fn::ImportValue: !Sub "${NetworkStack}-AlloySecurityGroupId" + Tags: + - Key: "environment" + Value: !Ref Environment + + FromLoadBalancerToLiteIngress: + Type: AWS::EC2::SecurityGroupIngress + Properties: + Description: "the ingress rule to allow traffic from the ELB to the Splits Lite containers" + GroupId: !Ref SecurityGroup + IpProtocol: tcp + FromPort: !Ref LitePort + ToPort: !Ref LitePort + SourceSecurityGroupId: + Fn::ImportValue: !Sub "${NetworkStack}-ELBSecurityGroupID" + Tags: + - Key: "environment" + Value: !Ref Environment + + FromLoadBalancerToLiteEgress: + Type: AWS::EC2::SecurityGroupEgress + Properties: + Description: "the egress rule to allow traffic from the ELB to the Splits Lite containers" + GroupId: + Fn::ImportValue: !Sub "${NetworkStack}-ELBSecurityGroupID" + IpProtocol: tcp + ToPort: !Ref LitePort + FromPort: !Ref LitePort + DestinationSecurityGroupId: !Ref SecurityGroup + Tags: + - Key: "environment" + Value: !Ref Environment + + # ServiceDiscovery exposes the private DNS for the Splits Lite service via + # Cloud Map within the environment specific VPC. Multiple tasks of the same + # service resolve to their respective private IPs, which can be accessed with + # the appropriate security group configuration. + # + # dig +short lite.splits.local + # + # 10.50.117.100 + # 10.50.114.203 + # 10.50.113.188 + # + ServiceDiscovery: + Type: AWS::ServiceDiscovery::Service + Properties: + Name: "lite" + NamespaceId: + Fn::ImportValue: !Sub "${DiscoveryStack}-DiscoveryNamespaceId" + DnsConfig: + DnsRecords: + - Type: A + TTL: 60 diff --git a/pkg/operator/infrastructure/preview/testdata/000/out.yaml.golden b/pkg/operator/infrastructure/preview/testdata/000/out.yaml.golden new file mode 100644 index 0000000..825a058 --- /dev/null +++ b/pkg/operator/infrastructure/preview/testdata/000/out.yaml.golden @@ -0,0 +1,413 @@ +AWSTemplateFormatVersion: "2010-09-09" +Description: "CloudFormation template for the Splits Lite containers and their load balancer configuration" + +Parameters: + Environment: + Description: "the name of the deployed environment" + Type: String + + DiscoveryStack: + Description: "the name of the discovery stack" + Type: String + + FargateStack: + Description: "the name of the server stack" + Type: String + + NetworkStack: + Description: "the name of the networking stack" + Type: String + + Secret: + Description: "the name of the environment/stack specific secret" + Type: String + + LiteCertificate: + Description: "the ARN of the SSL certificate for the Splits Lite containers" + Type: String + + LiteDomain: + Description: "the domain name of the Splits Lite DNS" + Type: String + + LitePort: + Description: "the server port of the Splits Lite containers" + Type: Number + + LiteVersion: + Description: "the Docker image tag for the Splits Lite containers" + Type: String + +Conditions: + IsProd: !Equals [!Ref Environment, production] + +Resources: + Cluster: + Type: AWS::ECS::Cluster + Properties: + ClusterName: !Ref AWS::StackName + Tags: + - Key: "environment" + Value: !Ref Environment + - Key: "service" + Value: "splits-lite" # must match repo name + + Service: + Type: AWS::ECS::Service + Properties: + Cluster: !Ref Cluster + ServiceName: !Sub "${AWS::StackName}-lite" + DesiredCount: 1 + LaunchType: "FARGATE" + TaskDefinition: !Ref TaskDefinition + NetworkConfiguration: + AwsvpcConfiguration: + AssignPublicIp: "DISABLED" + SecurityGroups: + - !Ref SecurityGroup + Subnets: + - Fn::ImportValue: !Sub "${NetworkStack}-PrivateSubnet1ID" + - Fn::ImportValue: !Sub "${NetworkStack}-PrivateSubnet2ID" + LoadBalancers: + - TargetGroupArn: !Ref TargetGroup + ContainerName: !Sub "${AWS::StackName}-lite" + ContainerPort: !Ref LitePort + ServiceRegistries: + - ContainerName: !Sub "${AWS::StackName}-lite" + RegistryArn: !GetAtt ServiceDiscovery.Arn + Tags: + - Key: "environment" + Value: !Ref Environment + - Key: "service" + Value: "splits-lite" # must match repo name + DependsOn: + - Cluster + + TaskDefinition: + Type: AWS::ECS::TaskDefinition + Properties: + Family: !Sub "${AWS::StackName}-lite" + RequiresCompatibilities: + - FARGATE + Cpu: 512 + Memory: 1024 + NetworkMode: awsvpc + ExecutionRoleArn: !GetAtt TaskExecutionRole.Arn + TaskRoleArn: !Sub "arn:aws:iam::${AWS::AccountId}:role/splits-ecs-role" + ContainerDefinitions: + - Name: !Sub "${AWS::StackName}-lite" + Image: !Sub "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/splits-lite:${LiteVersion}" + PortMappings: + - ContainerPort: !Ref LitePort + Protocol: "tcp" + Essential: true + Secrets: + - Name: ALCHEMY_API_KEY + ValueFrom: !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${Secret}:ALCHEMY_API_KEY::" + - Name: WALLETCONNECT_PROJECT_ID + ValueFrom: !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${Secret}:WALLETCONNECT_PROJECT_ID::" + LogConfiguration: + LogDriver: awslogs + Options: + awslogs-group: !Sub "/fargate/${AWS::StackName}/lite" + awslogs-region: !Ref AWS::Region + awslogs-stream-prefix: ecs + Tags: + - Key: "environment" + Value: !Ref Environment + + LogGroup: + Type: AWS::Logs::LogGroup + Properties: + LogGroupName: !Sub "/fargate/${AWS::StackName}/lite" + RetentionInDays: 7 + Tags: + - Key: "environment" + Value: !Ref Environment + + TaskExecutionRole: + Type: AWS::IAM::Role + Properties: + Path: "/" + AssumeRolePolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Action: sts:AssumeRole + Principal: + Service: ecs-tasks.amazonaws.com + ManagedPolicyArns: + - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy + Policies: + - PolicyName: secrets-access + PolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Action: + - secretsmanager:GetSecretValue + Resource: !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${Secret}*" + Tags: + - Key: "environment" + Value: !Ref Environment + + DomainRecord: + Type: AWS::Route53::RecordSet + Properties: + HostedZoneId: + Fn::ImportValue: !Sub "${NetworkStack}-EnvironmentHostedZoneId" + Name: !Ref LiteDomain + Type: A + AliasTarget: + DNSName: + Fn::ImportValue: !Sub "${FargateStack}-ApplicationLoadBalancerBaseDnsName" + HostedZoneId: + Fn::ImportValue: !Sub "${FargateStack}-ApplicationLoadBalancerCanonicalHostedZoneId" + + TargetGroup: + Type: AWS::ElasticLoadBalancingV2::TargetGroup + Properties: + Port: !Ref LitePort + Protocol: "HTTP" + VpcId: + Fn::ImportValue: !Sub "${NetworkStack}-VpcID" + TargetType: "ip" + HealthCheckPath: "/" + HealthCheckProtocol: "HTTP" + Matcher: + HttpCode: 200 + Tags: + - Key: "environment" + Value: !Ref Environment + + ListenerRule: + Type: AWS::ElasticLoadBalancingV2::ListenerRule + Properties: + Actions: + - Type: forward + TargetGroupArn: !Ref TargetGroup + Conditions: + - Field: host-header + HostHeaderConfig: + Values: + - !Ref LiteDomain + ListenerArn: + Fn::ImportValue: !Sub "${FargateStack}-ApplicationLoadBalancerListenerArn" + Priority: 12 # Host header = lite.{environment}.splits.org + + ListenerCertificate: + Type: AWS::ElasticLoadBalancingV2::ListenerCertificate + Properties: + Certificates: + - CertificateArn: !Ref LiteCertificate + ListenerArn: + Fn::ImportValue: !Sub "${FargateStack}-ApplicationLoadBalancerListenerArn" + + # ProdListenerRule registers the target group of the splits lite containers + # under the shorthand production domain lite.splits.org, but only for the + # production environment. Note that in order to make this work, we are also + # registering the respective SSL certificate in the load balancer, so that the + # TLS handshake may succeed. + ProdListenerRule: + Type: AWS::ElasticLoadBalancingV2::ListenerRule + Condition: IsProd + Properties: + Actions: + - Type: "forward" + TargetGroupArn: !Ref TargetGroup + Conditions: + - Field: "host-header" + HostHeaderConfig: + Values: + - "lite.splits.org" + ListenerArn: + Fn::ImportValue: !Sub "${FargateStack}-ApplicationLoadBalancerListenerArn" + Priority: 16 # Host header = lite.splits.org + + ProdListenerCertificate: + Condition: IsProd + Type: AWS::ElasticLoadBalancingV2::ListenerCertificate + Properties: + Certificates: + - CertificateArn: "arn:aws:acm:us-west-2:995626699990:certificate/4e1a3a6b-c119-4594-a6c9-7f667401d78e" + ListenerArn: + Fn::ImportValue: !Sub "${FargateStack}-ApplicationLoadBalancerListenerArn" + + SecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: "the security group for the Splits Lite containers in the Splits Lite stack" + VpcId: + Fn::ImportValue: !Sub "${NetworkStack}-VpcID" + Tags: + - Key: "environment" + Value: !Ref Environment + + FromAlloyToLiteIngress: + Type: AWS::EC2::SecurityGroupIngress + Properties: + Description: "the ingress rule to allow traffic from the Alloy to the Splits Lite containers" + GroupId: !Ref SecurityGroup + IpProtocol: "tcp" + FromPort: !Ref LitePort + ToPort: !Ref LitePort + SourceSecurityGroupId: + Fn::ImportValue: !Sub "${NetworkStack}-AlloySecurityGroupId" + Tags: + - Key: "environment" + Value: !Ref Environment + + FromLoadBalancerToLiteIngress: + Type: AWS::EC2::SecurityGroupIngress + Properties: + Description: "the ingress rule to allow traffic from the ELB to the Splits Lite containers" + GroupId: !Ref SecurityGroup + IpProtocol: tcp + FromPort: !Ref LitePort + ToPort: !Ref LitePort + SourceSecurityGroupId: + Fn::ImportValue: !Sub "${NetworkStack}-ELBSecurityGroupID" + Tags: + - Key: "environment" + Value: !Ref Environment + + FromLoadBalancerToLiteEgress: + Type: AWS::EC2::SecurityGroupEgress + Properties: + Description: "the egress rule to allow traffic from the ELB to the Splits Lite containers" + GroupId: + Fn::ImportValue: !Sub "${NetworkStack}-ELBSecurityGroupID" + IpProtocol: tcp + ToPort: !Ref LitePort + FromPort: !Ref LitePort + DestinationSecurityGroupId: !Ref SecurityGroup + Tags: + - Key: "environment" + Value: !Ref Environment + + # ServiceDiscovery exposes the private DNS for the Splits Lite service via + # Cloud Map within the environment specific VPC. Multiple tasks of the same + # service resolve to their respective private IPs, which can be accessed with + # the appropriate security group configuration. + # + # dig +short lite.splits.local + # + # 10.50.117.100 + # 10.50.114.203 + # 10.50.113.188 + # + ServiceDiscovery: + Type: AWS::ServiceDiscovery::Service + Properties: + Name: "lite" + NamespaceId: + Fn::ImportValue: !Sub "${DiscoveryStack}-DiscoveryNamespaceId" + DnsConfig: + DnsRecords: + - Type: A + TTL: 60 + + ServiceFancyFeatureBranch: + Type: AWS::ECS::Service + Properties: + Cluster: !Ref Cluster + ServiceName: !Sub "${AWS::StackName}-lite-fancy-feature-branch" # must be unique + DesiredCount: 1 + LaunchType: "FARGATE" + TaskDefinition: !Ref TaskDefinitionFancyFeatureBranch + NetworkConfiguration: + AwsvpcConfiguration: + AssignPublicIp: "DISABLED" + SecurityGroups: + - !Ref SecurityGroup + Subnets: + - Fn::ImportValue: !Sub "${NetworkStack}-PrivateSubnet1ID" + - Fn::ImportValue: !Sub "${NetworkStack}-PrivateSubnet2ID" + LoadBalancers: + - TargetGroupArn: !Ref TargetGroupFancyFeatureBranch + ContainerName: !Sub "${AWS::StackName}-lite" + ContainerPort: !Ref LitePort + Tags: + - Key: "environment" + Value: !Ref Environment + - Key: "service" + Value: "splits-lite" + DependsOn: + - Cluster + + TaskDefinitionFancyFeatureBranch: + Type: AWS::ECS::TaskDefinition + Properties: + Family: !Sub "${AWS::StackName}-lite-fancy-feature-branch" + RequiresCompatibilities: + - FARGATE + Cpu: 512 + Memory: 1024 + NetworkMode: awsvpc + ExecutionRoleArn: !GetAtt TaskExecutionRole.Arn + TaskRoleArn: !Sub "arn:aws:iam::${AWS::AccountId}:role/splits-ecs-role" + ContainerDefinitions: + - Name: !Sub "${AWS::StackName}-lite" + Image: !Sub "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/splits-lite:bc7891268e44f62e0aebbe339c0850b61d52c417" # https://github.com/0xSplits/splits-lite/pull/44 + PortMappings: + - ContainerPort: !Ref LitePort + Protocol: "tcp" + Essential: true + Secrets: + - Name: ALCHEMY_API_KEY + ValueFrom: !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${Secret}:ALCHEMY_API_KEY::" + - Name: WALLETCONNECT_PROJECT_ID + ValueFrom: !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${Secret}:WALLETCONNECT_PROJECT_ID::" + LogConfiguration: + LogDriver: awslogs + Options: + awslogs-group: !Sub "/fargate/${AWS::StackName}/lite" + awslogs-region: !Ref AWS::Region + awslogs-stream-prefix: ecs + Tags: + - Key: "environment" + Value: !Ref Environment + + DomainRecordFancyFeatureBranch: + Type: AWS::Route53::RecordSet + Properties: + HostedZoneId: + Fn::ImportValue: !Sub "${NetworkStack}-EnvironmentHostedZoneId" + Name: !Sub "fancy-feature-branch.lite.${Environment}.splits.org" + Type: A + AliasTarget: + DNSName: + Fn::ImportValue: !Sub "${FargateStack}-ApplicationLoadBalancerBaseDnsName" + HostedZoneId: + Fn::ImportValue: !Sub "${FargateStack}-ApplicationLoadBalancerCanonicalHostedZoneId" + + TargetGroupFancyFeatureBranch: + Type: AWS::ElasticLoadBalancingV2::TargetGroup + Properties: + Port: !Ref LitePort + Protocol: "HTTP" + VpcId: + Fn::ImportValue: !Sub "${NetworkStack}-VpcID" + TargetType: "ip" + HealthCheckPath: "/" + HealthCheckProtocol: "HTTP" + Matcher: + HttpCode: 200 + Tags: + - Key: "environment" + Value: !Ref Environment + + ListenerRuleFancyFeatureBranch: + Type: AWS::ElasticLoadBalancingV2::ListenerRule + Properties: + Actions: + - Type: forward + TargetGroupArn: !Ref TargetGroupFancyFeatureBranch + Conditions: + - Field: host-header + HostHeaderConfig: + Values: + - !Sub "fancy-feature-branch.lite.${Environment}.splits.org" + ListenerArn: + Fn::ImportValue: !Sub "${FargateStack}-ApplicationLoadBalancerListenerArn" + Priority: 17 # Host header = fancy-feature-branch.lite.{environment}.splits.org diff --git a/pkg/scanner/scanner.go b/pkg/scanner/scanner.go new file mode 100644 index 0000000..47d6129 --- /dev/null +++ b/pkg/scanner/scanner.go @@ -0,0 +1,26 @@ +// Package scanner is a multi line block scanner for YAML input bytes. +package scanner + +import ( + "fmt" + + "github.com/xh3b4sd/tracer" +) + +type Config struct { + Inp []byte +} + +type Scanner struct { + inp []byte +} + +func New(c Config) *Scanner { + if c.Inp == nil { + tracer.Panic(tracer.Mask(fmt.Errorf("%T.Inp must not be empty", c))) + } + + return &Scanner{ + inp: c.Inp, + } +} diff --git a/pkg/scanner/search.go b/pkg/scanner/search.go new file mode 100644 index 0000000..77746d2 --- /dev/null +++ b/pkg/scanner/search.go @@ -0,0 +1,65 @@ +package scanner + +import ( + "bufio" + "bytes" + "unicode" +) + +// Search tries to find the entire YAML block identified by the given key line, +// e.g. " Service:". +func (s *Scanner) Search(key []byte) []byte { + var sca *bufio.Scanner + { + sca = bufio.NewScanner(bytes.NewReader(s.inp)) + } + + var blo [][]byte + var fou bool + var end int + var sta int + for sca.Scan() { + var lin []byte + { + lin = sca.Bytes() + } + + if fou { + end = spaces(lin) + } + + if fou && sta == end && len(lin) != 0 { + break + } + + if !fou { + fou = bytes.Equal(lin, key) + sta = spaces(lin) + } + + if fou { + blo = append(blo, lin) + } + } + + var res []byte + { + res = bytes.Join(blo, []byte("\n")) + } + + return res +} + +func spaces(b []byte) int { + var cou int + + for _, x := range b { + if unicode.IsSpace(rune(x)) { + cou++ + } else { + break + } + } + + return cou +} diff --git a/pkg/scanner/search_test.go b/pkg/scanner/search_test.go new file mode 100644 index 0000000..abd8bfd --- /dev/null +++ b/pkg/scanner/search_test.go @@ -0,0 +1,71 @@ +package scanner + +import ( + "bytes" + "fmt" + "os" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func Test_Scanner_Search(t *testing.T) { + testCases := []struct { + key string + }{ + // Case 000 + { + key: " Service:", + }, + // Case 001 + { + key: " TaskDefinition:", + }, + // Case 002 + { + key: "Resources:", + }, + // Case 003 + { + key: " ServiceRegistries:", + }, + } + + for i, tc := range testCases { + t.Run(fmt.Sprintf("%03d", i), func(t *testing.T) { + var err error + + var inp []byte + { + inp, err = os.ReadFile(fmt.Sprintf("./testdata/%03d/inp.yaml.golden", i)) + if err != nil { + t.Fatal("expected", nil, "got", err) + } + } + + var out []byte + { + out, err = os.ReadFile(fmt.Sprintf("./testdata/%03d/out.yaml.golden", i)) + if err != nil { + t.Fatal("expected", nil, "got", err) + } + } + + var sca *Scanner + { + sca = New(Config{ + Inp: inp, + }) + } + + var res []byte + { + res = sca.Search([]byte(tc.key)) + } + + if dif := cmp.Diff(bytes.TrimSpace(out), bytes.TrimSpace(res)); dif != "" { + t.Fatalf("-expected +actual:\n%s", dif) + } + }) + } +} diff --git a/pkg/scanner/testdata/000/inp.yaml.golden b/pkg/scanner/testdata/000/inp.yaml.golden new file mode 100644 index 0000000..fc0c9c6 --- /dev/null +++ b/pkg/scanner/testdata/000/inp.yaml.golden @@ -0,0 +1,15 @@ +AWSTemplateFormatVersion: "2010-09-09" + +Resources: + Cluster: + Type: AWS::ECS::Cluster + + Service: + Type: AWS::ECS::Service + + TaskDefinition: + Type: AWS::ECS::TaskDefinition + +Outputs: + ServiceVersion: + Value: "v0.2.0" diff --git a/pkg/scanner/testdata/000/out.yaml.golden b/pkg/scanner/testdata/000/out.yaml.golden new file mode 100644 index 0000000..05791d0 --- /dev/null +++ b/pkg/scanner/testdata/000/out.yaml.golden @@ -0,0 +1,2 @@ + Service: + Type: AWS::ECS::Service diff --git a/pkg/scanner/testdata/001/inp.yaml.golden b/pkg/scanner/testdata/001/inp.yaml.golden new file mode 100644 index 0000000..bfedc9b --- /dev/null +++ b/pkg/scanner/testdata/001/inp.yaml.golden @@ -0,0 +1,23 @@ + + + +AWSTemplateFormatVersion: "2010-09-09" + +Resources: + TaskDefinition: + + Type: AWS::ECS::TaskDefinition + + Cluster: + Type: AWS::ECS::Cluster + + Service: + + + + Type: AWS::ECS::Service + +Outputs: + + ServiceVersion: + Value: "v0.2.0" diff --git a/pkg/scanner/testdata/001/out.yaml.golden b/pkg/scanner/testdata/001/out.yaml.golden new file mode 100644 index 0000000..a7182d4 --- /dev/null +++ b/pkg/scanner/testdata/001/out.yaml.golden @@ -0,0 +1,3 @@ + TaskDefinition: + + Type: AWS::ECS::TaskDefinition diff --git a/pkg/scanner/testdata/002/inp.yaml.golden b/pkg/scanner/testdata/002/inp.yaml.golden new file mode 100644 index 0000000..fc0c9c6 --- /dev/null +++ b/pkg/scanner/testdata/002/inp.yaml.golden @@ -0,0 +1,15 @@ +AWSTemplateFormatVersion: "2010-09-09" + +Resources: + Cluster: + Type: AWS::ECS::Cluster + + Service: + Type: AWS::ECS::Service + + TaskDefinition: + Type: AWS::ECS::TaskDefinition + +Outputs: + ServiceVersion: + Value: "v0.2.0" diff --git a/pkg/scanner/testdata/002/out.yaml.golden b/pkg/scanner/testdata/002/out.yaml.golden new file mode 100644 index 0000000..b07d9f4 --- /dev/null +++ b/pkg/scanner/testdata/002/out.yaml.golden @@ -0,0 +1,9 @@ +Resources: + Cluster: + Type: AWS::ECS::Cluster + + Service: + Type: AWS::ECS::Service + + TaskDefinition: + Type: AWS::ECS::TaskDefinition diff --git a/pkg/scanner/testdata/003/inp.yaml.golden b/pkg/scanner/testdata/003/inp.yaml.golden new file mode 100644 index 0000000..bf01872 --- /dev/null +++ b/pkg/scanner/testdata/003/inp.yaml.golden @@ -0,0 +1,32 @@ + Service: + Type: AWS::ECS::Service + Properties: + Cluster: !Ref Cluster + ServiceName: !Sub "${AWS::StackName}-lite" + DesiredCount: 1 + LaunchType: "FARGATE" + TaskDefinition: !Ref TaskDefinition + NetworkConfiguration: + AwsvpcConfiguration: + AssignPublicIp: "DISABLED" + SecurityGroups: + - !Ref SecurityGroup + Subnets: + - Fn::ImportValue: !Sub "${NetworkStack}-PrivateSubnet1ID" + - Fn::ImportValue: !Sub "${NetworkStack}-PrivateSubnet2ID" + LoadBalancers: + - TargetGroupArn: !Ref TargetGroup + ContainerName: !Sub "${AWS::StackName}-lite" + ContainerPort: !Ref LitePort + ServiceRegistries: + - ContainerName: !Sub "${AWS::StackName}-lite" + RegistryArn: !GetAtt ServiceDiscovery.Arn + Tags: + - Key: "environment" + Value: !Ref Environment + - Key: "service" + Value: "splits-lite" # must match repo name + DependsOn: + - Cluster + - TaskDefinition + - ListenerRule diff --git a/pkg/scanner/testdata/003/out.yaml.golden b/pkg/scanner/testdata/003/out.yaml.golden new file mode 100644 index 0000000..dab60df --- /dev/null +++ b/pkg/scanner/testdata/003/out.yaml.golden @@ -0,0 +1,3 @@ + ServiceRegistries: + - ContainerName: !Sub "${AWS::StackName}-lite" + RegistryArn: !GetAtt ServiceDiscovery.Arn From 30595c6cb9afc1c25fd1ff1bfbf8954b9f2d141d Mon Sep 17 00:00:00 2001 From: xh3b4sd Date: Thu, 11 Sep 2025 17:08:47 +0200 Subject: [PATCH 02/13] fix --- log.json | 36 ------------------------------------ 1 file changed, 36 deletions(-) delete mode 100644 log.json diff --git a/log.json b/log.json deleted file mode 100644 index 925e683..0000000 --- a/log.json +++ /dev/null @@ -1,36 +0,0 @@ -[ - { - "time": "2025-09-01 14:47:24", - "level": "warning", - "message": "git ref unresolvable", - "branch": "lite", - "owner": "0xSplits", - "reason": "branch not found", - "repository": "infrastructure", - "suggestion": "this issue might be caused by a user error or eventual consistency of the underlying backend", - "caller": "/build/pkg/operator/reference/github.go:42" - }, - { - "time": "2025-09-01 14:44:06", - "level": "error", - "message": "worker execution failed", - "stack": { - "context": [ - { - "key": "handler", - "value": "policy" - } - ], - "description": "This critical error indicates that some cached state of the current reconciliation loop was missing, which means that the operator does not know how to proceed safely.", - "trace": [ - "/build/pkg/operator/policy/ensure.go:42", - "/build/pkg/operator/policy/ensure.go:12", - "/go/pkg/mod/github.com/0x!splits/workit@v0.5.0/handler/metrics/ensure.go:44", - "/go/pkg/mod/github.com/0x!splits/workit@v0.5.0/worker/sequence/ensure.go:79", - "/go/pkg/mod/github.com/0x!splits/workit@v0.5.0/worker/sequence/ensure.go:24", - "/go/pkg/mod/github.com/0x!splits/workit@v0.5.0/worker/sequence/daemon.go:23" - ] - }, - "caller": "/go/pkg/mod/github.com/0x!splits/workit@v0.5.0/worker/sequence/daemon.go:39" - } -] From ae636447d9ccc5b5377cfeca3d6f52b0b79d4535 Mon Sep 17 00:00:00 2001 From: xh3b4sd Date: Fri, 12 Sep 2025 23:29:42 +0200 Subject: [PATCH 03/13] fix --- go.mod | 5 +- go.sum | 2 - pkg/operator/infrastructure/preview/hash.go | 12 +++ pkg/operator/infrastructure/preview/render.go | 61 +++++++------ .../infrastructure/preview/render_test.go | 2 + .../infrastructure/preview/resource.go | 12 +-- .../preview/testdata/000/inp.yaml.golden | 2 +- .../preview/testdata/000/out.yaml.golden | 31 +++---- pkg/scanner/append.go | 66 ++++++++++++++ pkg/scanner/append_test.go | 86 +++++++++++++++++++ pkg/scanner/bytes.go | 6 ++ pkg/scanner/delete.go | 53 ++++++++++++ pkg/scanner/delete_test.go | 71 +++++++++++++++ pkg/scanner/search.go | 21 +++-- pkg/scanner/search_test.go | 6 +- .../testdata/{ => append}/000/inp.yaml.golden | 0 .../testdata/append/000/out.yaml.golden | 15 ++++ .../testdata/append/001/inp.yaml.golden | 23 +++++ .../testdata/append/001/out.yaml.golden | 23 +++++ .../testdata/append/002/inp.yaml.golden | 18 ++++ .../testdata/append/002/out.yaml.golden | 18 ++++ .../testdata/{ => append}/003/inp.yaml.golden | 0 .../testdata/append/003/out.yaml.golden | 32 +++++++ .../testdata/append/004/inp.yaml.golden | 32 +++++++ .../testdata/append/004/out.yaml.golden | 32 +++++++ .../testdata/append/005/inp.yaml.golden | 32 +++++++ .../testdata/append/005/out.yaml.golden | 32 +++++++ .../{002 => delete/000}/inp.yaml.golden | 0 .../testdata/delete/000/out.yaml.golden | 12 +++ .../testdata/delete/001/inp.yaml.golden | 19 ++++ .../testdata/delete/001/out.yaml.golden | 16 ++++ .../testdata/delete/002/inp.yaml.golden | 19 ++++ .../testdata/delete/002/out.yaml.golden | 5 ++ .../testdata/delete/003/inp.yaml.golden | 32 +++++++ .../testdata/delete/003/out.yaml.golden | 29 +++++++ .../testdata/search/000/inp.yaml.golden | 15 ++++ .../testdata/{ => search}/000/out.yaml.golden | 0 .../testdata/{ => search}/001/inp.yaml.golden | 0 .../testdata/{ => search}/001/out.yaml.golden | 0 .../testdata/search/002/inp.yaml.golden | 15 ++++ .../testdata/{ => search}/002/out.yaml.golden | 0 .../testdata/search/003/inp.yaml.golden | 32 +++++++ .../testdata/{ => search}/003/out.yaml.golden | 0 43 files changed, 820 insertions(+), 67 deletions(-) create mode 100644 pkg/operator/infrastructure/preview/hash.go create mode 100644 pkg/scanner/append.go create mode 100644 pkg/scanner/append_test.go create mode 100644 pkg/scanner/bytes.go create mode 100644 pkg/scanner/delete.go create mode 100644 pkg/scanner/delete_test.go rename pkg/scanner/testdata/{ => append}/000/inp.yaml.golden (100%) create mode 100644 pkg/scanner/testdata/append/000/out.yaml.golden create mode 100644 pkg/scanner/testdata/append/001/inp.yaml.golden create mode 100644 pkg/scanner/testdata/append/001/out.yaml.golden create mode 100644 pkg/scanner/testdata/append/002/inp.yaml.golden create mode 100644 pkg/scanner/testdata/append/002/out.yaml.golden rename pkg/scanner/testdata/{ => append}/003/inp.yaml.golden (100%) create mode 100644 pkg/scanner/testdata/append/003/out.yaml.golden create mode 100644 pkg/scanner/testdata/append/004/inp.yaml.golden create mode 100644 pkg/scanner/testdata/append/004/out.yaml.golden create mode 100644 pkg/scanner/testdata/append/005/inp.yaml.golden create mode 100644 pkg/scanner/testdata/append/005/out.yaml.golden rename pkg/scanner/testdata/{002 => delete/000}/inp.yaml.golden (100%) create mode 100644 pkg/scanner/testdata/delete/000/out.yaml.golden create mode 100644 pkg/scanner/testdata/delete/001/inp.yaml.golden create mode 100644 pkg/scanner/testdata/delete/001/out.yaml.golden create mode 100644 pkg/scanner/testdata/delete/002/inp.yaml.golden create mode 100644 pkg/scanner/testdata/delete/002/out.yaml.golden create mode 100644 pkg/scanner/testdata/delete/003/inp.yaml.golden create mode 100644 pkg/scanner/testdata/delete/003/out.yaml.golden create mode 100644 pkg/scanner/testdata/search/000/inp.yaml.golden rename pkg/scanner/testdata/{ => search}/000/out.yaml.golden (100%) rename pkg/scanner/testdata/{ => search}/001/inp.yaml.golden (100%) rename pkg/scanner/testdata/{ => search}/001/out.yaml.golden (100%) create mode 100644 pkg/scanner/testdata/search/002/inp.yaml.golden rename pkg/scanner/testdata/{ => search}/002/out.yaml.golden (100%) create mode 100644 pkg/scanner/testdata/search/003/inp.yaml.golden rename pkg/scanner/testdata/{ => search}/003/out.yaml.golden (100%) diff --git a/go.mod b/go.mod index 5708073..501fcc3 100644 --- a/go.mod +++ b/go.mod @@ -2,8 +2,6 @@ module github.com/0xSplits/kayron go 1.24.0 -toolchain go1.24.6 - require ( github.com/0xSplits/otelgo v0.1.2 github.com/0xSplits/roghfs v0.1.0 @@ -18,6 +16,7 @@ require ( github.com/distribution/reference v0.6.0 github.com/goccy/go-yaml v1.18.0 github.com/google/go-cmp v0.7.0 + github.com/google/go-containerregistry v0.20.6 github.com/google/go-github/v73 v73.0.0 github.com/gorilla/mux v1.8.1 github.com/joho/godotenv v1.5.1 @@ -58,11 +57,9 @@ require ( github.com/docker/docker-credential-helpers v0.9.3 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/google/go-containerregistry v0.20.6 github.com/google/go-querystring v1.1.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 // indirect - github.com/iancoleman/strcase v0.3.0 github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect diff --git a/go.sum b/go.sum index 54b1337..69c5f05 100644 --- a/go.sum +++ b/go.sum @@ -90,8 +90,6 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 h1:cLN4IBkmkYZNnk7EAJ0BHIethd+J6LqxFNw5mSiI2bM= github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk= -github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= -github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= diff --git a/pkg/operator/infrastructure/preview/hash.go b/pkg/operator/infrastructure/preview/hash.go new file mode 100644 index 0000000..f53a936 --- /dev/null +++ b/pkg/operator/infrastructure/preview/hash.go @@ -0,0 +1,12 @@ +package preview + +import ( + "crypto/sha256" + "encoding/hex" +) + +func Hash(str string) string { + sum := sha256.Sum256([]byte(str)) + enc := hex.EncodeToString(sum[:]) + return enc[:8] +} diff --git a/pkg/operator/infrastructure/preview/render.go b/pkg/operator/infrastructure/preview/render.go index 67927dd..39bd9d9 100644 --- a/pkg/operator/infrastructure/preview/render.go +++ b/pkg/operator/infrastructure/preview/render.go @@ -1,12 +1,5 @@ package preview -import ( - "bytes" - "fmt" - - "github.com/iancoleman/strcase" -) - func (p *Preview) Render(bra []string) []byte { var res Resource { @@ -31,38 +24,50 @@ func (p *Preview) Render(bra []string) []byte { return out } +// TODO scanner needs Replace and Delete func (p *Preview) render(res Resource, bra string) []byte { var out []byte - var cam string - var keb string + var hsh string { - cam = strcase.ToCamel(bra) // FancyFeatureBranch - keb = strcase.ToKebab(bra) // fancy-feature-branch + hsh = Hash(bra) } - fmt.Printf("%#v\n", cam) - fmt.Printf("%#v\n", keb) + { + res.Ser = res.Ser.Append([]byte(" Service:"), []byte(hsh)) + res.Ser = res.Ser.Append([]byte(" ServiceName:"), []byte("-"+hsh)) + res.Ser = res.Ser.Append([]byte(" TaskDefinition:"), []byte(hsh)) + res.Ser = res.Ser.Append([]byte(" - TargetGroupArn:"), []byte(hsh)) + } + + { + res.Tas = res.Tas.Append([]byte(" TaskDefinition:"), []byte(hsh)) + res.Tas = res.Tas.Append([]byte(" Family:"), []byte("-"+hsh)) + } + + { + res.Dom = res.Dom.Append([]byte(" DomainRecord:"), []byte(hsh)) + } + + { + res.Tar = res.Tar.Append([]byte(" TargetGroup:"), []byte(hsh)) + } { - res.Ser = bytes.Replace(res.Ser, []byte(" Service:"), fmt.Appendf(nil, " Service%s:", cam), 1) - res.Tas = bytes.Replace(res.Tas, []byte(" TaskDefinition:"), fmt.Appendf(nil, " TaskDefinition%s:", cam), 1) - res.Dom = bytes.Replace(res.Dom, []byte(" DomainRecord:"), fmt.Appendf(nil, " DomainRecord%s:", cam), 1) - res.Tar = bytes.Replace(res.Tar, []byte(" TargetGroup:"), fmt.Appendf(nil, " TargetGroup%s:", cam), 1) - res.Lis = bytes.Replace(res.Lis, []byte(" ListenerRule:"), fmt.Appendf(nil, " ListenerRule%s:", cam), 1) + res.Lis = res.Lis.Append([]byte(" ListenerRule:"), []byte(hsh)) } { - out = append(out, '\n') - out = append(out, res.Ser...) - out = append(out, '\n') - out = append(out, res.Tas...) - out = append(out, '\n') - out = append(out, res.Dom...) - out = append(out, '\n') - out = append(out, res.Tar...) - out = append(out, '\n') - out = append(out, res.Lis...) + out = append(out, '\n', '\n') + out = append(out, res.Ser.Bytes()...) + out = append(out, '\n', '\n') + out = append(out, res.Tas.Bytes()...) + out = append(out, '\n', '\n') + out = append(out, res.Dom.Bytes()...) + out = append(out, '\n', '\n') + out = append(out, res.Tar.Bytes()...) + out = append(out, '\n', '\n') + out = append(out, res.Lis.Bytes()...) } return out diff --git a/pkg/operator/infrastructure/preview/render_test.go b/pkg/operator/infrastructure/preview/render_test.go index f9fc712..18b7f2c 100644 --- a/pkg/operator/infrastructure/preview/render_test.go +++ b/pkg/operator/infrastructure/preview/render_test.go @@ -9,6 +9,8 @@ import ( "github.com/google/go-cmp/cmp" ) +// TODO test more branch names with non standard characters + func Test_Operator_Infrastructure_Preview_Render(t *testing.T) { testCases := []struct { bra []string diff --git a/pkg/operator/infrastructure/preview/resource.go b/pkg/operator/infrastructure/preview/resource.go index e47c473..00d4d3d 100644 --- a/pkg/operator/infrastructure/preview/resource.go +++ b/pkg/operator/infrastructure/preview/resource.go @@ -1,9 +1,11 @@ package preview +import "github.com/0xSplits/kayron/pkg/scanner" + type Resource struct { - Ser []byte - Tas []byte - Dom []byte - Tar []byte - Lis []byte + Ser *scanner.Scanner + Tas *scanner.Scanner + Dom *scanner.Scanner + Tar *scanner.Scanner + Lis *scanner.Scanner } diff --git a/pkg/operator/infrastructure/preview/testdata/000/inp.yaml.golden b/pkg/operator/infrastructure/preview/testdata/000/inp.yaml.golden index 6914ac6..e8ecafe 100644 --- a/pkg/operator/infrastructure/preview/testdata/000/inp.yaml.golden +++ b/pkg/operator/infrastructure/preview/testdata/000/inp.yaml.golden @@ -79,7 +79,7 @@ Resources: - Key: "environment" Value: !Ref Environment - Key: "service" - Value: "splits-lite" # must match repo name + Value: "splits-lite" DependsOn: - Cluster diff --git a/pkg/operator/infrastructure/preview/testdata/000/out.yaml.golden b/pkg/operator/infrastructure/preview/testdata/000/out.yaml.golden index 825a058..5c6fb84 100644 --- a/pkg/operator/infrastructure/preview/testdata/000/out.yaml.golden +++ b/pkg/operator/infrastructure/preview/testdata/000/out.yaml.golden @@ -79,7 +79,7 @@ Resources: - Key: "environment" Value: !Ref Environment - Key: "service" - Value: "splits-lite" # must match repo name + Value: "splits-lite" DependsOn: - Cluster @@ -307,14 +307,15 @@ Resources: - Type: A TTL: 60 - ServiceFancyFeatureBranch: + + Service1d0fd508: Type: AWS::ECS::Service Properties: Cluster: !Ref Cluster - ServiceName: !Sub "${AWS::StackName}-lite-fancy-feature-branch" # must be unique + ServiceName: !Sub "${AWS::StackName}-lite-1d0fd508" DesiredCount: 1 LaunchType: "FARGATE" - TaskDefinition: !Ref TaskDefinitionFancyFeatureBranch + TaskDefinition: !Ref TaskDefinition1d0fd508 NetworkConfiguration: AwsvpcConfiguration: AssignPublicIp: "DISABLED" @@ -324,7 +325,7 @@ Resources: - Fn::ImportValue: !Sub "${NetworkStack}-PrivateSubnet1ID" - Fn::ImportValue: !Sub "${NetworkStack}-PrivateSubnet2ID" LoadBalancers: - - TargetGroupArn: !Ref TargetGroupFancyFeatureBranch + - TargetGroupArn: !Ref TargetGroup1d0fd508 ContainerName: !Sub "${AWS::StackName}-lite" ContainerPort: !Ref LitePort Tags: @@ -335,10 +336,10 @@ Resources: DependsOn: - Cluster - TaskDefinitionFancyFeatureBranch: + TaskDefinition1d0fd508: Type: AWS::ECS::TaskDefinition Properties: - Family: !Sub "${AWS::StackName}-lite-fancy-feature-branch" + Family: !Sub "${AWS::StackName}-lite-1d0fd508" RequiresCompatibilities: - FARGATE Cpu: 512 @@ -348,7 +349,7 @@ Resources: TaskRoleArn: !Sub "arn:aws:iam::${AWS::AccountId}:role/splits-ecs-role" ContainerDefinitions: - Name: !Sub "${AWS::StackName}-lite" - Image: !Sub "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/splits-lite:bc7891268e44f62e0aebbe339c0850b61d52c417" # https://github.com/0xSplits/splits-lite/pull/44 + Image: !Sub "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/splits-lite:bc7891268e44f62e0aebbe339c0850b61d52c417" PortMappings: - ContainerPort: !Ref LitePort Protocol: "tcp" @@ -368,12 +369,12 @@ Resources: - Key: "environment" Value: !Ref Environment - DomainRecordFancyFeatureBranch: + DomainRecord1d0fd508: Type: AWS::Route53::RecordSet Properties: HostedZoneId: Fn::ImportValue: !Sub "${NetworkStack}-EnvironmentHostedZoneId" - Name: !Sub "fancy-feature-branch.lite.${Environment}.splits.org" + Name: !Sub "1d0fd508.lite.${Environment}.splits.org" Type: A AliasTarget: DNSName: @@ -381,7 +382,7 @@ Resources: HostedZoneId: Fn::ImportValue: !Sub "${FargateStack}-ApplicationLoadBalancerCanonicalHostedZoneId" - TargetGroupFancyFeatureBranch: + TargetGroup1d0fd508: Type: AWS::ElasticLoadBalancingV2::TargetGroup Properties: Port: !Ref LitePort @@ -397,17 +398,17 @@ Resources: - Key: "environment" Value: !Ref Environment - ListenerRuleFancyFeatureBranch: + ListenerRule1d0fd508: Type: AWS::ElasticLoadBalancingV2::ListenerRule Properties: Actions: - Type: forward - TargetGroupArn: !Ref TargetGroupFancyFeatureBranch + TargetGroupArn: !Ref TargetGroup1d0fd508 Conditions: - Field: host-header HostHeaderConfig: Values: - - !Sub "fancy-feature-branch.lite.${Environment}.splits.org" + - !Sub "1d0fd508.lite.${Environment}.splits.org" ListenerArn: Fn::ImportValue: !Sub "${FargateStack}-ApplicationLoadBalancerListenerArn" - Priority: 17 # Host header = fancy-feature-branch.lite.{environment}.splits.org + Priority: 17 # Host header = 1d0fd508.lite.{environment}.splits.org diff --git a/pkg/scanner/append.go b/pkg/scanner/append.go new file mode 100644 index 0000000..9efebb1 --- /dev/null +++ b/pkg/scanner/append.go @@ -0,0 +1,66 @@ +package scanner + +import ( + "bufio" + "bytes" +) + +func (s *Scanner) Append(pre []byte, suf []byte) *Scanner { + var buf *bufio.Scanner + { + buf = bufio.NewScanner(bytes.NewReader(s.inp)) + } + + var blo [][]byte + for buf.Scan() { + var lin []byte + { + lin = append([]byte(nil), buf.Bytes()...) // copy to prevent buffer overwrites + } + + if bytes.HasPrefix(lin, pre) { + lin = insert(lin, suf) + } + + { + blo = append(blo, lin) + } + } + + var inp []byte + { + inp = bytes.Join(blo, []byte("\n")) + } + + return New(Config{ + Inp: inp, + }) +} + +func insert(lin []byte, suf []byte) []byte { + var las byte + { + las = lin[len(lin)-1] + } + + // double quote 0x22 + // single quote 0x27 + // colon 0x3A + + if las == 0x22 || las == 0x27 || las == 0x3A { + return merge(lin[:len(lin)-1], suf, []byte{las}) + } + + return append(lin, suf...) +} + +func merge(pre []byte, mid []byte, suf []byte) []byte { + out := make([]byte, len(pre)+len(mid)+len(suf)) + + num := copy(out, pre) + num += copy(out[num:], mid) + + copy(out[num:], suf) + + return out +} diff --git a/pkg/scanner/append_test.go b/pkg/scanner/append_test.go new file mode 100644 index 0000000..dc19d80 --- /dev/null +++ b/pkg/scanner/append_test.go @@ -0,0 +1,86 @@ +package scanner + +import ( + "bytes" + "fmt" + "os" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func Test_Scanner_Append(t *testing.T) { + testCases := []struct { + pre string + suf string + }{ + // Case 000 + { + pre: " Value:", + suf: "-1234", + }, + // Case 001 + { + pre: " Value:", + suf: ".0xFa73", + }, + // Case 002 + { + pre: " ServiceName:", + suf: "-1d0fd508", + }, + // Case 003 + { + pre: " TaskDefinition:", + suf: "-e3eae11", + }, + // Case 004 + { + pre: " TaskDefinition:", + suf: "-XHEKSOUDL", + }, + // Case 005 + { + pre: " Service:", + suf: "FancyFeatureBranch", + }, + } + + for i, tc := range testCases { + t.Run(fmt.Sprintf("%03d", i), func(t *testing.T) { + var err error + + var inp []byte + { + inp, err = os.ReadFile(fmt.Sprintf("./testdata/append/%03d/inp.yaml.golden", i)) + if err != nil { + t.Fatal("expected", nil, "got", err) + } + } + + var out []byte + { + out, err = os.ReadFile(fmt.Sprintf("./testdata/append/%03d/out.yaml.golden", i)) + if err != nil { + t.Fatal("expected", nil, "got", err) + } + } + + var sca *Scanner + { + sca = New(Config{ + Inp: inp, + }) + } + + var res []byte + { + res = sca.Append([]byte(tc.pre), []byte(tc.suf)).Bytes() + } + + if dif := cmp.Diff(bytes.TrimSpace(out), bytes.TrimSpace(res)); dif != "" { + t.Fatalf("-expected +actual:\n%s", dif) + } + }) + } +} diff --git a/pkg/scanner/bytes.go b/pkg/scanner/bytes.go new file mode 100644 index 0000000..e58dbd0 --- /dev/null +++ b/pkg/scanner/bytes.go @@ -0,0 +1,6 @@ +package scanner + +// Bytes returns the input bytes that this scanner is conigured with. +func (s *Scanner) Bytes() []byte { + return s.inp +} diff --git a/pkg/scanner/delete.go b/pkg/scanner/delete.go new file mode 100644 index 0000000..61c7e1a --- /dev/null +++ b/pkg/scanner/delete.go @@ -0,0 +1,53 @@ +package scanner + +import ( + "bufio" + "bytes" +) + +// Delete tries to drop the entire YAML block identified by the given key line, +// e.g. " Service:". A new scanner configured without the found YAML block as +// input bytes is returned. +func (s *Scanner) Delete(key []byte) *Scanner { + var buf *bufio.Scanner + { + buf = bufio.NewScanner(bytes.NewReader(s.inp)) + } + + var blo [][]byte + var drp bool + var end int + var sta int + for buf.Scan() { + var lin []byte + { + lin = append([]byte(nil), buf.Bytes()...) // copy to prevent buffer overwrites + } + + if drp { + end = spaces(lin) + } + + if drp && end <= sta && len(lin) != 0 { + drp = false + } + + if bytes.Equal(lin, key) { + drp = true + sta = spaces(lin) + } + + if !drp { + blo = append(blo, lin) + } + } + + var inp []byte + { + inp = bytes.Join(blo, []byte("\n")) + } + + return New(Config{ + Inp: inp, + }) +} diff --git a/pkg/scanner/delete_test.go b/pkg/scanner/delete_test.go new file mode 100644 index 0000000..01eb2b2 --- /dev/null +++ b/pkg/scanner/delete_test.go @@ -0,0 +1,71 @@ +package scanner + +import ( + "bytes" + "fmt" + "os" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func Test_Scanner_Delete(t *testing.T) { + testCases := []struct { + pre string + }{ + // Case 000 + { + pre: " Service:", + }, + // Case 001 + { + pre: " TaskDefinition:", + }, + // Case 002 + { + pre: "Resources:", + }, + // Case 003 + { + pre: " ServiceRegistries:", + }, + } + + for i, tc := range testCases { + t.Run(fmt.Sprintf("%03d", i), func(t *testing.T) { + var err error + + var inp []byte + { + inp, err = os.ReadFile(fmt.Sprintf("./testdata/delete/%03d/inp.yaml.golden", i)) + if err != nil { + t.Fatal("expected", nil, "got", err) + } + } + + var out []byte + { + out, err = os.ReadFile(fmt.Sprintf("./testdata/delete/%03d/out.yaml.golden", i)) + if err != nil { + t.Fatal("expected", nil, "got", err) + } + } + + var sca *Scanner + { + sca = New(Config{ + Inp: inp, + }) + } + + var res []byte + { + res = sca.Delete([]byte(tc.pre)).Bytes() + } + + if dif := cmp.Diff(bytes.TrimSpace(out), bytes.TrimSpace(res)); dif != "" { + t.Fatalf("-expected +actual:\n%s", dif) + } + }) + } +} diff --git a/pkg/scanner/search.go b/pkg/scanner/search.go index 77746d2..2eba010 100644 --- a/pkg/scanner/search.go +++ b/pkg/scanner/search.go @@ -7,21 +7,22 @@ import ( ) // Search tries to find the entire YAML block identified by the given key line, -// e.g. " Service:". -func (s *Scanner) Search(key []byte) []byte { - var sca *bufio.Scanner +// e.g. " Service:". A new scanner configured with the found YAML block as +// input bytes is returned. +func (s *Scanner) Search(key []byte) *Scanner { + var buf *bufio.Scanner { - sca = bufio.NewScanner(bytes.NewReader(s.inp)) + buf = bufio.NewScanner(bytes.NewReader(s.inp)) } var blo [][]byte var fou bool var end int var sta int - for sca.Scan() { + for buf.Scan() { var lin []byte { - lin = sca.Bytes() + lin = append([]byte(nil), buf.Bytes()...) // copy to prevent buffer overwrites } if fou { @@ -42,12 +43,14 @@ func (s *Scanner) Search(key []byte) []byte { } } - var res []byte + var inp []byte { - res = bytes.Join(blo, []byte("\n")) + inp = bytes.Join(blo, []byte("\n")) } - return res + return New(Config{ + Inp: inp, + }) } func spaces(b []byte) int { diff --git a/pkg/scanner/search_test.go b/pkg/scanner/search_test.go index abd8bfd..91094ea 100644 --- a/pkg/scanner/search_test.go +++ b/pkg/scanner/search_test.go @@ -37,7 +37,7 @@ func Test_Scanner_Search(t *testing.T) { var inp []byte { - inp, err = os.ReadFile(fmt.Sprintf("./testdata/%03d/inp.yaml.golden", i)) + inp, err = os.ReadFile(fmt.Sprintf("./testdata/search/%03d/inp.yaml.golden", i)) if err != nil { t.Fatal("expected", nil, "got", err) } @@ -45,7 +45,7 @@ func Test_Scanner_Search(t *testing.T) { var out []byte { - out, err = os.ReadFile(fmt.Sprintf("./testdata/%03d/out.yaml.golden", i)) + out, err = os.ReadFile(fmt.Sprintf("./testdata/search/%03d/out.yaml.golden", i)) if err != nil { t.Fatal("expected", nil, "got", err) } @@ -60,7 +60,7 @@ func Test_Scanner_Search(t *testing.T) { var res []byte { - res = sca.Search([]byte(tc.key)) + res = sca.Search([]byte(tc.key)).Bytes() } if dif := cmp.Diff(bytes.TrimSpace(out), bytes.TrimSpace(res)); dif != "" { diff --git a/pkg/scanner/testdata/000/inp.yaml.golden b/pkg/scanner/testdata/append/000/inp.yaml.golden similarity index 100% rename from pkg/scanner/testdata/000/inp.yaml.golden rename to pkg/scanner/testdata/append/000/inp.yaml.golden diff --git a/pkg/scanner/testdata/append/000/out.yaml.golden b/pkg/scanner/testdata/append/000/out.yaml.golden new file mode 100644 index 0000000..e3d8e94 --- /dev/null +++ b/pkg/scanner/testdata/append/000/out.yaml.golden @@ -0,0 +1,15 @@ +AWSTemplateFormatVersion: "2010-09-09" + +Resources: + Cluster: + Type: AWS::ECS::Cluster + + Service: + Type: AWS::ECS::Service + + TaskDefinition: + Type: AWS::ECS::TaskDefinition + +Outputs: + ServiceVersion: + Value: "v0.2.0-1234" diff --git a/pkg/scanner/testdata/append/001/inp.yaml.golden b/pkg/scanner/testdata/append/001/inp.yaml.golden new file mode 100644 index 0000000..c0bac0e --- /dev/null +++ b/pkg/scanner/testdata/append/001/inp.yaml.golden @@ -0,0 +1,23 @@ + + + +AWSTemplateFormatVersion: "2010-09-09" + +Resources: + TaskDefinition: + + Type: AWS::ECS::TaskDefinition + + Cluster: + Type: AWS::ECS::Cluster + + Service: + + + + Type: AWS::ECS::Service + +Outputs: + + ServiceVersion: + Value: 'v0.2.0' diff --git a/pkg/scanner/testdata/append/001/out.yaml.golden b/pkg/scanner/testdata/append/001/out.yaml.golden new file mode 100644 index 0000000..396e35d --- /dev/null +++ b/pkg/scanner/testdata/append/001/out.yaml.golden @@ -0,0 +1,23 @@ + + + +AWSTemplateFormatVersion: "2010-09-09" + +Resources: + TaskDefinition: + + Type: AWS::ECS::TaskDefinition + + Cluster: + Type: AWS::ECS::Cluster + + Service: + + + + Type: AWS::ECS::Service + +Outputs: + + ServiceVersion: + Value: 'v0.2.0.0xFa73' diff --git a/pkg/scanner/testdata/append/002/inp.yaml.golden b/pkg/scanner/testdata/append/002/inp.yaml.golden new file mode 100644 index 0000000..d3a96f2 --- /dev/null +++ b/pkg/scanner/testdata/append/002/inp.yaml.golden @@ -0,0 +1,18 @@ +AWSTemplateFormatVersion: "2010-09-09" + +Resources: + Cluster: + Type: AWS::ECS::Cluster + + Service: + Type: AWS::ECS::Service + Properties: + Cluster: !Ref Cluster + ServiceName: !Sub "${AWS::StackName}-lite" + + TaskDefinition: + Type: AWS::ECS::TaskDefinition + +Outputs: + ServiceVersion: + Value: "v0.2.0" diff --git a/pkg/scanner/testdata/append/002/out.yaml.golden b/pkg/scanner/testdata/append/002/out.yaml.golden new file mode 100644 index 0000000..a6aaa8e --- /dev/null +++ b/pkg/scanner/testdata/append/002/out.yaml.golden @@ -0,0 +1,18 @@ +AWSTemplateFormatVersion: "2010-09-09" + +Resources: + Cluster: + Type: AWS::ECS::Cluster + + Service: + Type: AWS::ECS::Service + Properties: + Cluster: !Ref Cluster + ServiceName: !Sub "${AWS::StackName}-lite-1d0fd508" + + TaskDefinition: + Type: AWS::ECS::TaskDefinition + +Outputs: + ServiceVersion: + Value: "v0.2.0" diff --git a/pkg/scanner/testdata/003/inp.yaml.golden b/pkg/scanner/testdata/append/003/inp.yaml.golden similarity index 100% rename from pkg/scanner/testdata/003/inp.yaml.golden rename to pkg/scanner/testdata/append/003/inp.yaml.golden diff --git a/pkg/scanner/testdata/append/003/out.yaml.golden b/pkg/scanner/testdata/append/003/out.yaml.golden new file mode 100644 index 0000000..95fbc33 --- /dev/null +++ b/pkg/scanner/testdata/append/003/out.yaml.golden @@ -0,0 +1,32 @@ + Service: + Type: AWS::ECS::Service + Properties: + Cluster: !Ref Cluster + ServiceName: !Sub "${AWS::StackName}-lite" + DesiredCount: 1 + LaunchType: "FARGATE" + TaskDefinition: !Ref TaskDefinition-e3eae11 + NetworkConfiguration: + AwsvpcConfiguration: + AssignPublicIp: "DISABLED" + SecurityGroups: + - !Ref SecurityGroup + Subnets: + - Fn::ImportValue: !Sub "${NetworkStack}-PrivateSubnet1ID" + - Fn::ImportValue: !Sub "${NetworkStack}-PrivateSubnet2ID" + LoadBalancers: + - TargetGroupArn: !Ref TargetGroup + ContainerName: !Sub "${AWS::StackName}-lite" + ContainerPort: !Ref LitePort + ServiceRegistries: + - ContainerName: !Sub "${AWS::StackName}-lite" + RegistryArn: !GetAtt ServiceDiscovery.Arn + Tags: + - Key: "environment" + Value: !Ref Environment + - Key: "service" + Value: "splits-lite" # must match repo name + DependsOn: + - Cluster + - TaskDefinition + - ListenerRule diff --git a/pkg/scanner/testdata/append/004/inp.yaml.golden b/pkg/scanner/testdata/append/004/inp.yaml.golden new file mode 100644 index 0000000..0c82dd9 --- /dev/null +++ b/pkg/scanner/testdata/append/004/inp.yaml.golden @@ -0,0 +1,32 @@ + Service: + Type: AWS::ECS::Service + Properties: + Cluster: !Ref Cluster + ServiceName: !Sub "${AWS::StackName}-lite" + DesiredCount: 1 + LaunchType: "FARGATE" + TaskDefinition: !Ref "TaskDefinition" + NetworkConfiguration: + AwsvpcConfiguration: + AssignPublicIp: "DISABLED" + SecurityGroups: + - !Ref SecurityGroup + Subnets: + - Fn::ImportValue: !Sub "${NetworkStack}-PrivateSubnet1ID" + - Fn::ImportValue: !Sub "${NetworkStack}-PrivateSubnet2ID" + LoadBalancers: + - TargetGroupArn: !Ref TargetGroup + ContainerName: !Sub "${AWS::StackName}-lite" + ContainerPort: !Ref LitePort + ServiceRegistries: + - ContainerName: !Sub "${AWS::StackName}-lite" + RegistryArn: !GetAtt ServiceDiscovery.Arn + Tags: + - Key: "environment" + Value: !Ref Environment + - Key: "service" + Value: "splits-lite" # must match repo name + DependsOn: + - Cluster + - TaskDefinition + - ListenerRule diff --git a/pkg/scanner/testdata/append/004/out.yaml.golden b/pkg/scanner/testdata/append/004/out.yaml.golden new file mode 100644 index 0000000..3a23df2 --- /dev/null +++ b/pkg/scanner/testdata/append/004/out.yaml.golden @@ -0,0 +1,32 @@ + Service: + Type: AWS::ECS::Service + Properties: + Cluster: !Ref Cluster + ServiceName: !Sub "${AWS::StackName}-lite" + DesiredCount: 1 + LaunchType: "FARGATE" + TaskDefinition: !Ref "TaskDefinition-XHEKSOUDL" + NetworkConfiguration: + AwsvpcConfiguration: + AssignPublicIp: "DISABLED" + SecurityGroups: + - !Ref SecurityGroup + Subnets: + - Fn::ImportValue: !Sub "${NetworkStack}-PrivateSubnet1ID" + - Fn::ImportValue: !Sub "${NetworkStack}-PrivateSubnet2ID" + LoadBalancers: + - TargetGroupArn: !Ref TargetGroup + ContainerName: !Sub "${AWS::StackName}-lite" + ContainerPort: !Ref LitePort + ServiceRegistries: + - ContainerName: !Sub "${AWS::StackName}-lite" + RegistryArn: !GetAtt ServiceDiscovery.Arn + Tags: + - Key: "environment" + Value: !Ref Environment + - Key: "service" + Value: "splits-lite" # must match repo name + DependsOn: + - Cluster + - TaskDefinition + - ListenerRule diff --git a/pkg/scanner/testdata/append/005/inp.yaml.golden b/pkg/scanner/testdata/append/005/inp.yaml.golden new file mode 100644 index 0000000..bf01872 --- /dev/null +++ b/pkg/scanner/testdata/append/005/inp.yaml.golden @@ -0,0 +1,32 @@ + Service: + Type: AWS::ECS::Service + Properties: + Cluster: !Ref Cluster + ServiceName: !Sub "${AWS::StackName}-lite" + DesiredCount: 1 + LaunchType: "FARGATE" + TaskDefinition: !Ref TaskDefinition + NetworkConfiguration: + AwsvpcConfiguration: + AssignPublicIp: "DISABLED" + SecurityGroups: + - !Ref SecurityGroup + Subnets: + - Fn::ImportValue: !Sub "${NetworkStack}-PrivateSubnet1ID" + - Fn::ImportValue: !Sub "${NetworkStack}-PrivateSubnet2ID" + LoadBalancers: + - TargetGroupArn: !Ref TargetGroup + ContainerName: !Sub "${AWS::StackName}-lite" + ContainerPort: !Ref LitePort + ServiceRegistries: + - ContainerName: !Sub "${AWS::StackName}-lite" + RegistryArn: !GetAtt ServiceDiscovery.Arn + Tags: + - Key: "environment" + Value: !Ref Environment + - Key: "service" + Value: "splits-lite" # must match repo name + DependsOn: + - Cluster + - TaskDefinition + - ListenerRule diff --git a/pkg/scanner/testdata/append/005/out.yaml.golden b/pkg/scanner/testdata/append/005/out.yaml.golden new file mode 100644 index 0000000..aac98b1 --- /dev/null +++ b/pkg/scanner/testdata/append/005/out.yaml.golden @@ -0,0 +1,32 @@ + ServiceFancyFeatureBranch: + Type: AWS::ECS::Service + Properties: + Cluster: !Ref Cluster + ServiceName: !Sub "${AWS::StackName}-lite" + DesiredCount: 1 + LaunchType: "FARGATE" + TaskDefinition: !Ref TaskDefinition + NetworkConfiguration: + AwsvpcConfiguration: + AssignPublicIp: "DISABLED" + SecurityGroups: + - !Ref SecurityGroup + Subnets: + - Fn::ImportValue: !Sub "${NetworkStack}-PrivateSubnet1ID" + - Fn::ImportValue: !Sub "${NetworkStack}-PrivateSubnet2ID" + LoadBalancers: + - TargetGroupArn: !Ref TargetGroup + ContainerName: !Sub "${AWS::StackName}-lite" + ContainerPort: !Ref LitePort + ServiceRegistries: + - ContainerName: !Sub "${AWS::StackName}-lite" + RegistryArn: !GetAtt ServiceDiscovery.Arn + Tags: + - Key: "environment" + Value: !Ref Environment + - Key: "service" + Value: "splits-lite" # must match repo name + DependsOn: + - Cluster + - TaskDefinition + - ListenerRule diff --git a/pkg/scanner/testdata/002/inp.yaml.golden b/pkg/scanner/testdata/delete/000/inp.yaml.golden similarity index 100% rename from pkg/scanner/testdata/002/inp.yaml.golden rename to pkg/scanner/testdata/delete/000/inp.yaml.golden diff --git a/pkg/scanner/testdata/delete/000/out.yaml.golden b/pkg/scanner/testdata/delete/000/out.yaml.golden new file mode 100644 index 0000000..1ef0541 --- /dev/null +++ b/pkg/scanner/testdata/delete/000/out.yaml.golden @@ -0,0 +1,12 @@ +AWSTemplateFormatVersion: "2010-09-09" + +Resources: + Cluster: + Type: AWS::ECS::Cluster + + TaskDefinition: + Type: AWS::ECS::TaskDefinition + +Outputs: + ServiceVersion: + Value: "v0.2.0" diff --git a/pkg/scanner/testdata/delete/001/inp.yaml.golden b/pkg/scanner/testdata/delete/001/inp.yaml.golden new file mode 100644 index 0000000..f35d7f7 --- /dev/null +++ b/pkg/scanner/testdata/delete/001/inp.yaml.golden @@ -0,0 +1,19 @@ + + + +AWSTemplateFormatVersion: "2010-09-09" + +Resources: + TaskDefinition: + Type: AWS::ECS::TaskDefinition + + Cluster: + Type: AWS::ECS::Cluster + + Service: + Type: AWS::ECS::Service + +Outputs: + + ServiceVersion: + Value: "v0.2.0" diff --git a/pkg/scanner/testdata/delete/001/out.yaml.golden b/pkg/scanner/testdata/delete/001/out.yaml.golden new file mode 100644 index 0000000..d6f4b84 --- /dev/null +++ b/pkg/scanner/testdata/delete/001/out.yaml.golden @@ -0,0 +1,16 @@ + + + +AWSTemplateFormatVersion: "2010-09-09" + +Resources: + Cluster: + Type: AWS::ECS::Cluster + + Service: + Type: AWS::ECS::Service + +Outputs: + + ServiceVersion: + Value: "v0.2.0" diff --git a/pkg/scanner/testdata/delete/002/inp.yaml.golden b/pkg/scanner/testdata/delete/002/inp.yaml.golden new file mode 100644 index 0000000..66d360c --- /dev/null +++ b/pkg/scanner/testdata/delete/002/inp.yaml.golden @@ -0,0 +1,19 @@ +AWSTemplateFormatVersion: "2010-09-09" + +Resources: + Cluster: + Type: AWS::ECS::Cluster + + Service: + + Type: AWS::ECS::Service + + TaskDefinition: + + + + Type: AWS::ECS::TaskDefinition + +Outputs: + ServiceVersion: + Value: "v0.2.0" diff --git a/pkg/scanner/testdata/delete/002/out.yaml.golden b/pkg/scanner/testdata/delete/002/out.yaml.golden new file mode 100644 index 0000000..911288a --- /dev/null +++ b/pkg/scanner/testdata/delete/002/out.yaml.golden @@ -0,0 +1,5 @@ +AWSTemplateFormatVersion: "2010-09-09" + +Outputs: + ServiceVersion: + Value: "v0.2.0" diff --git a/pkg/scanner/testdata/delete/003/inp.yaml.golden b/pkg/scanner/testdata/delete/003/inp.yaml.golden new file mode 100644 index 0000000..bf01872 --- /dev/null +++ b/pkg/scanner/testdata/delete/003/inp.yaml.golden @@ -0,0 +1,32 @@ + Service: + Type: AWS::ECS::Service + Properties: + Cluster: !Ref Cluster + ServiceName: !Sub "${AWS::StackName}-lite" + DesiredCount: 1 + LaunchType: "FARGATE" + TaskDefinition: !Ref TaskDefinition + NetworkConfiguration: + AwsvpcConfiguration: + AssignPublicIp: "DISABLED" + SecurityGroups: + - !Ref SecurityGroup + Subnets: + - Fn::ImportValue: !Sub "${NetworkStack}-PrivateSubnet1ID" + - Fn::ImportValue: !Sub "${NetworkStack}-PrivateSubnet2ID" + LoadBalancers: + - TargetGroupArn: !Ref TargetGroup + ContainerName: !Sub "${AWS::StackName}-lite" + ContainerPort: !Ref LitePort + ServiceRegistries: + - ContainerName: !Sub "${AWS::StackName}-lite" + RegistryArn: !GetAtt ServiceDiscovery.Arn + Tags: + - Key: "environment" + Value: !Ref Environment + - Key: "service" + Value: "splits-lite" # must match repo name + DependsOn: + - Cluster + - TaskDefinition + - ListenerRule diff --git a/pkg/scanner/testdata/delete/003/out.yaml.golden b/pkg/scanner/testdata/delete/003/out.yaml.golden new file mode 100644 index 0000000..1cd6743 --- /dev/null +++ b/pkg/scanner/testdata/delete/003/out.yaml.golden @@ -0,0 +1,29 @@ + Service: + Type: AWS::ECS::Service + Properties: + Cluster: !Ref Cluster + ServiceName: !Sub "${AWS::StackName}-lite" + DesiredCount: 1 + LaunchType: "FARGATE" + TaskDefinition: !Ref TaskDefinition + NetworkConfiguration: + AwsvpcConfiguration: + AssignPublicIp: "DISABLED" + SecurityGroups: + - !Ref SecurityGroup + Subnets: + - Fn::ImportValue: !Sub "${NetworkStack}-PrivateSubnet1ID" + - Fn::ImportValue: !Sub "${NetworkStack}-PrivateSubnet2ID" + LoadBalancers: + - TargetGroupArn: !Ref TargetGroup + ContainerName: !Sub "${AWS::StackName}-lite" + ContainerPort: !Ref LitePort + Tags: + - Key: "environment" + Value: !Ref Environment + - Key: "service" + Value: "splits-lite" # must match repo name + DependsOn: + - Cluster + - TaskDefinition + - ListenerRule diff --git a/pkg/scanner/testdata/search/000/inp.yaml.golden b/pkg/scanner/testdata/search/000/inp.yaml.golden new file mode 100644 index 0000000..fc0c9c6 --- /dev/null +++ b/pkg/scanner/testdata/search/000/inp.yaml.golden @@ -0,0 +1,15 @@ +AWSTemplateFormatVersion: "2010-09-09" + +Resources: + Cluster: + Type: AWS::ECS::Cluster + + Service: + Type: AWS::ECS::Service + + TaskDefinition: + Type: AWS::ECS::TaskDefinition + +Outputs: + ServiceVersion: + Value: "v0.2.0" diff --git a/pkg/scanner/testdata/000/out.yaml.golden b/pkg/scanner/testdata/search/000/out.yaml.golden similarity index 100% rename from pkg/scanner/testdata/000/out.yaml.golden rename to pkg/scanner/testdata/search/000/out.yaml.golden diff --git a/pkg/scanner/testdata/001/inp.yaml.golden b/pkg/scanner/testdata/search/001/inp.yaml.golden similarity index 100% rename from pkg/scanner/testdata/001/inp.yaml.golden rename to pkg/scanner/testdata/search/001/inp.yaml.golden diff --git a/pkg/scanner/testdata/001/out.yaml.golden b/pkg/scanner/testdata/search/001/out.yaml.golden similarity index 100% rename from pkg/scanner/testdata/001/out.yaml.golden rename to pkg/scanner/testdata/search/001/out.yaml.golden diff --git a/pkg/scanner/testdata/search/002/inp.yaml.golden b/pkg/scanner/testdata/search/002/inp.yaml.golden new file mode 100644 index 0000000..fc0c9c6 --- /dev/null +++ b/pkg/scanner/testdata/search/002/inp.yaml.golden @@ -0,0 +1,15 @@ +AWSTemplateFormatVersion: "2010-09-09" + +Resources: + Cluster: + Type: AWS::ECS::Cluster + + Service: + Type: AWS::ECS::Service + + TaskDefinition: + Type: AWS::ECS::TaskDefinition + +Outputs: + ServiceVersion: + Value: "v0.2.0" diff --git a/pkg/scanner/testdata/002/out.yaml.golden b/pkg/scanner/testdata/search/002/out.yaml.golden similarity index 100% rename from pkg/scanner/testdata/002/out.yaml.golden rename to pkg/scanner/testdata/search/002/out.yaml.golden diff --git a/pkg/scanner/testdata/search/003/inp.yaml.golden b/pkg/scanner/testdata/search/003/inp.yaml.golden new file mode 100644 index 0000000..bf01872 --- /dev/null +++ b/pkg/scanner/testdata/search/003/inp.yaml.golden @@ -0,0 +1,32 @@ + Service: + Type: AWS::ECS::Service + Properties: + Cluster: !Ref Cluster + ServiceName: !Sub "${AWS::StackName}-lite" + DesiredCount: 1 + LaunchType: "FARGATE" + TaskDefinition: !Ref TaskDefinition + NetworkConfiguration: + AwsvpcConfiguration: + AssignPublicIp: "DISABLED" + SecurityGroups: + - !Ref SecurityGroup + Subnets: + - Fn::ImportValue: !Sub "${NetworkStack}-PrivateSubnet1ID" + - Fn::ImportValue: !Sub "${NetworkStack}-PrivateSubnet2ID" + LoadBalancers: + - TargetGroupArn: !Ref TargetGroup + ContainerName: !Sub "${AWS::StackName}-lite" + ContainerPort: !Ref LitePort + ServiceRegistries: + - ContainerName: !Sub "${AWS::StackName}-lite" + RegistryArn: !GetAtt ServiceDiscovery.Arn + Tags: + - Key: "environment" + Value: !Ref Environment + - Key: "service" + Value: "splits-lite" # must match repo name + DependsOn: + - Cluster + - TaskDefinition + - ListenerRule diff --git a/pkg/scanner/testdata/003/out.yaml.golden b/pkg/scanner/testdata/search/003/out.yaml.golden similarity index 100% rename from pkg/scanner/testdata/003/out.yaml.golden rename to pkg/scanner/testdata/search/003/out.yaml.golden From 9c8531960019ad894066fb0dac70fce7ba9b50a7 Mon Sep 17 00:00:00 2001 From: xh3b4sd Date: Mon, 15 Sep 2025 17:40:38 +0200 Subject: [PATCH 04/13] fix --- pkg/hash/hash.go | 25 ++++ pkg/operator/infrastructure/preview/error.go | 9 ++ pkg/operator/infrastructure/preview/hash.go | 12 -- pkg/operator/infrastructure/preview/image.go | 29 ++++ .../infrastructure/preview/image_test.go | 59 ++++++++ pkg/operator/infrastructure/preview/render.go | 95 +++++++++--- .../infrastructure/preview/render_test.go | 42 +++++- .../preview/testdata/000/out.yaml.golden | 138 ++++++++++++++++-- pkg/scanner/delete.go | 11 +- pkg/scanner/delete_test.go | 24 ++- pkg/scanner/search.go | 2 +- pkg/scanner/search_test.go | 4 + .../testdata/delete/004/inp.yaml.golden | 32 ++++ .../testdata/delete/004/out.yaml.golden | 31 ++++ .../testdata/delete/005/inp.yaml.golden | 32 ++++ .../testdata/delete/005/out.yaml.golden | 32 ++++ .../testdata/delete/006/inp.yaml.golden | 32 ++++ .../testdata/delete/006/out.yaml.golden | 17 +++ .../testdata/search/004/inp.yaml.golden | 32 ++++ .../testdata/search/004/out.yaml.golden | 1 + 20 files changed, 601 insertions(+), 58 deletions(-) create mode 100644 pkg/hash/hash.go create mode 100644 pkg/operator/infrastructure/preview/error.go delete mode 100644 pkg/operator/infrastructure/preview/hash.go create mode 100644 pkg/operator/infrastructure/preview/image.go create mode 100644 pkg/operator/infrastructure/preview/image_test.go create mode 100644 pkg/scanner/testdata/delete/004/inp.yaml.golden create mode 100644 pkg/scanner/testdata/delete/004/out.yaml.golden create mode 100644 pkg/scanner/testdata/delete/005/inp.yaml.golden create mode 100644 pkg/scanner/testdata/delete/005/out.yaml.golden create mode 100644 pkg/scanner/testdata/delete/006/inp.yaml.golden create mode 100644 pkg/scanner/testdata/delete/006/out.yaml.golden create mode 100644 pkg/scanner/testdata/search/004/inp.yaml.golden create mode 100644 pkg/scanner/testdata/search/004/out.yaml.golden diff --git a/pkg/hash/hash.go b/pkg/hash/hash.go new file mode 100644 index 0000000..14a0f49 --- /dev/null +++ b/pkg/hash/hash.go @@ -0,0 +1,25 @@ +package hash + +import ( + "crypto/sha256" + "encoding/hex" + + "golang.org/x/text/cases" + "golang.org/x/text/language" +) + +type Hash struct { + Hsh []byte + Dsh []byte +} + +func New(str string) Hash { + sum := sha256.Sum256([]byte(str)) + enc := hex.EncodeToString(sum[:]) + cas := cases.Upper(language.English).String(enc[:8]) + + return Hash{ + Hsh: []byte(cas), + Dsh: []byte("-" + cas), + } +} diff --git a/pkg/operator/infrastructure/preview/error.go b/pkg/operator/infrastructure/preview/error.go new file mode 100644 index 0000000..990046b --- /dev/null +++ b/pkg/operator/infrastructure/preview/error.go @@ -0,0 +1,9 @@ +package preview + +import ( + "github.com/xh3b4sd/tracer" +) + +var containerImageFormatError = &tracer.Error{ + Description: "This critical error indicates that the provided container image was unrecognizable, which means that the operator does not know how to proceed safely.", +} diff --git a/pkg/operator/infrastructure/preview/hash.go b/pkg/operator/infrastructure/preview/hash.go deleted file mode 100644 index f53a936..0000000 --- a/pkg/operator/infrastructure/preview/hash.go +++ /dev/null @@ -1,12 +0,0 @@ -package preview - -import ( - "crypto/sha256" - "encoding/hex" -) - -func Hash(str string) string { - sum := sha256.Sum256([]byte(str)) - enc := hex.EncodeToString(sum[:]) - return enc[:8] -} diff --git a/pkg/operator/infrastructure/preview/image.go b/pkg/operator/infrastructure/preview/image.go new file mode 100644 index 0000000..de30e15 --- /dev/null +++ b/pkg/operator/infrastructure/preview/image.go @@ -0,0 +1,29 @@ +package preview + +import ( + "bytes" + "regexp" + + "github.com/xh3b4sd/tracer" +) + +var ( + exp = regexp.MustCompile(`.*\/.*:(\$\{[^}]+\}|[A-Za-z0-9_][A-Za-z0-9._-]{0,127})($|["']?[ \t]*#.*|["'])`) +) + +func repIma(lin []byte, tag []byte) ([]byte, error) { + var sub [][]byte + { + sub = exp.FindSubmatch(lin) + if len(sub) < 2 { + return nil, tracer.Mask(containerImageFormatError, tracer.Context{Key: "input", Value: string(lin)}) + } + } + + var out []byte + { + out = bytes.ReplaceAll(lin, sub[1], tag) + } + + return out, nil +} diff --git a/pkg/operator/infrastructure/preview/image_test.go b/pkg/operator/infrastructure/preview/image_test.go new file mode 100644 index 0000000..1f41145 --- /dev/null +++ b/pkg/operator/infrastructure/preview/image_test.go @@ -0,0 +1,59 @@ +package preview + +import ( + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func Test_Operator_Infrastructure_Preview_Image(t *testing.T) { + testCases := []struct { + lin []byte + tag []byte + out []byte + }{ + // Case 000 + { + lin: []byte(`Image: !Sub "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/splits-lite:${LiteVersion}"`), + tag: []byte(`v0.2.0`), + out: []byte(`Image: !Sub "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/splits-lite:v0.2.0"`), + }, + // Case 001 + { + lin: []byte(` Image: 'ecr.amazonaws.com/splits-lite:${LiteVersion}' # hello world`), + tag: []byte(`v1.0.0`), + out: []byte(` Image: 'ecr.amazonaws.com/splits-lite:v1.0.0' # hello world`), + }, + // Case 002 + { + lin: []byte(` Image: !Sub "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/splits-lite:v0.1.0"`), + tag: []byte(`bc7891268e44f62e0aebbe339c0850b61d52c417`), + out: []byte(` Image: !Sub "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/splits-lite:bc7891268e44f62e0aebbe339c0850b61d52c417"`), + }, + // Case 003 + { + lin: []byte(`Image: !Sub '${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/splits-lite:bc7891268e44f62e0aebbe339c0850b61d52c417' # comment`), + tag: []byte(`v3.5.0-bc789126`), + out: []byte(`Image: !Sub '${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/splits-lite:v3.5.0-bc789126' # comment`), + }, + } + + for i, tc := range testCases { + t.Run(fmt.Sprintf("%03d", i), func(t *testing.T) { + var err error + + var out []byte + { + out, err = repIma(tc.lin, tc.tag) + if err != nil { + t.Fatal("expected", nil, "got", err) + } + } + + if dif := cmp.Diff(tc.out, out); dif != "" { + t.Fatalf("-expected +actual:\n%s", dif) + } + }) + } +} diff --git a/pkg/operator/infrastructure/preview/render.go b/pkg/operator/infrastructure/preview/render.go index 39bd9d9..765ced6 100644 --- a/pkg/operator/infrastructure/preview/render.go +++ b/pkg/operator/infrastructure/preview/render.go @@ -1,6 +1,28 @@ package preview -func (p *Preview) Render(bra []string) []byte { +import ( + "bytes" + "fmt" + + "github.com/0xSplits/kayron/pkg/cache" + "github.com/0xSplits/kayron/pkg/hash" + "github.com/xh3b4sd/tracer" +) + +var ( + header = bytes.Join( + [][]byte{ + []byte(" #"), + []byte(" # AUTO GENERATED PREVIEW DEPLOYMENT"), + []byte(" #"), + }, + []byte("\n"), + ) +) + +func (p *Preview) Render(art []cache.Object) ([]byte, error) { + var err error + var res Resource { res = Resource{ @@ -14,50 +36,80 @@ func (p *Preview) Render(bra []string) []byte { var out []byte { - out = p.inp + out = append(p.inp, '\n') } - for _, x := range bra { - out = append(out, p.render(res, x)...) + var pri int + { + pri = 30 } - return out -} + for _, x := range art { + var hsh hash.Hash + { + hsh = hash.New(x.Release.Deploy.Branch.String()) + } -// TODO scanner needs Replace and Delete -func (p *Preview) render(res Resource, bra string) []byte { - var out []byte + var dom string + { + dom = fmt.Sprintf("%s.%s.${Environment}.splits.org", hsh.Hsh, x.Release.Docker.String()) + } - var hsh string - { - hsh = Hash(bra) + var ima []byte + { + ima, err = repIma(res.Tas.Search([]byte(" Image:")).Bytes(), []byte(x.Artifact.Reference.Desired)) + if err != nil { + return nil, tracer.Mask(err) + } + } + + { + out = append(out, p.render(res, dom, pri, hsh, ima)...) + } + + { + pri++ + } } + return out, nil +} + +// TODO scanner needs Replace and Delete +func (p *Preview) render(res Resource, dom string, pri int, hsh hash.Hash, ima []byte) []byte { { - res.Ser = res.Ser.Append([]byte(" Service:"), []byte(hsh)) - res.Ser = res.Ser.Append([]byte(" ServiceName:"), []byte("-"+hsh)) - res.Ser = res.Ser.Append([]byte(" TaskDefinition:"), []byte(hsh)) - res.Ser = res.Ser.Append([]byte(" - TargetGroupArn:"), []byte(hsh)) + res.Ser = res.Ser.Append([]byte(" Service:"), hsh.Hsh) + res.Ser = res.Ser.Append([]byte(" ServiceName:"), hsh.Dsh) + res.Ser = res.Ser.Append([]byte(" TaskDefinition:"), hsh.Hsh) + res.Ser = res.Ser.Append([]byte(" - TargetGroupArn:"), hsh.Hsh) + res.Ser = res.Ser.Delete([]byte(" ServiceRegistries:")) } { - res.Tas = res.Tas.Append([]byte(" TaskDefinition:"), []byte(hsh)) - res.Tas = res.Tas.Append([]byte(" Family:"), []byte("-"+hsh)) + res.Tas = res.Tas.Append([]byte(" TaskDefinition:"), hsh.Hsh) + res.Tas = res.Tas.Append([]byte(" Family:"), hsh.Dsh) + res.Tas = res.Tas.Delete([]byte(" Image:"), ima...) } { - res.Dom = res.Dom.Append([]byte(" DomainRecord:"), []byte(hsh)) + res.Dom = res.Dom.Append([]byte(" DomainRecord:"), hsh.Hsh) + res.Dom = res.Dom.Delete([]byte(" Name:"), fmt.Appendf(nil, ` Name: !Sub "%s"`, dom)...) } { - res.Tar = res.Tar.Append([]byte(" TargetGroup:"), []byte(hsh)) + res.Tar = res.Tar.Append([]byte(" TargetGroup:"), hsh.Hsh) } { - res.Lis = res.Lis.Append([]byte(" ListenerRule:"), []byte(hsh)) + res.Lis = res.Lis.Append([]byte(" ListenerRule:"), hsh.Hsh) + res.Lis = res.Lis.Append([]byte(" TargetGroupArn:"), hsh.Hsh) + res.Lis = res.Lis.Delete([]byte(" Values:"), fmt.Appendf(nil, " Values:\n - !Sub \"%s\"", dom)...) + res.Lis = res.Lis.Delete([]byte(" Priority:"), fmt.Appendf(nil, " Priority: %d # Host header = %s", pri, dom)...) } + var out []byte { + out = append(out, header...) out = append(out, '\n', '\n') out = append(out, res.Ser.Bytes()...) out = append(out, '\n', '\n') @@ -68,6 +120,7 @@ func (p *Preview) render(res Resource, bra string) []byte { out = append(out, res.Tar.Bytes()...) out = append(out, '\n', '\n') out = append(out, res.Lis.Bytes()...) + out = append(out, '\n', '\n') } return out diff --git a/pkg/operator/infrastructure/preview/render_test.go b/pkg/operator/infrastructure/preview/render_test.go index 18b7f2c..9da48bd 100644 --- a/pkg/operator/infrastructure/preview/render_test.go +++ b/pkg/operator/infrastructure/preview/render_test.go @@ -6,19 +6,49 @@ import ( "os" "testing" + "github.com/0xSplits/kayron/pkg/cache" + "github.com/0xSplits/kayron/pkg/release/artifact" + "github.com/0xSplits/kayron/pkg/release/artifact/reference" + "github.com/0xSplits/kayron/pkg/release/schema/release" + "github.com/0xSplits/kayron/pkg/release/schema/release/deploy" + "github.com/0xSplits/kayron/pkg/release/schema/release/deploy/branch" + "github.com/0xSplits/kayron/pkg/release/schema/release/docker" "github.com/google/go-cmp/cmp" ) -// TODO test more branch names with non standard characters - func Test_Operator_Infrastructure_Preview_Render(t *testing.T) { testCases := []struct { - bra []string + obj []cache.Object }{ // Case 000 { - bra: []string{ - "fancy-feature-branch", + obj: []cache.Object{ + { + Artifact: artifact.Struct{ + Reference: reference.Struct{ + Desired: "bc7891268e44f62e0aebbe339c0850b61d52c417", + }, + }, + Release: release.Struct{ + Deploy: deploy.Struct{ + Branch: branch.String("fancy-feature-branch"), + }, + Docker: docker.String("lite"), + }, + }, + { + Artifact: artifact.Struct{ + Reference: reference.Struct{ + Desired: "02b42b7ec63d4078767cb3b7cb0d34fde91b6237", + }, + }, + Release: release.Struct{ + Deploy: deploy.Struct{ + Branch: branch.String("dependabot/another-one"), + }, + Docker: docker.String("lite"), + }, + }, }, }, } @@ -52,7 +82,7 @@ func Test_Operator_Infrastructure_Preview_Render(t *testing.T) { var res []byte { - res = pre.Render(tc.bra) + res, err = pre.Render(tc.obj) if err != nil { t.Fatal("expected", nil, "got", err) } diff --git a/pkg/operator/infrastructure/preview/testdata/000/out.yaml.golden b/pkg/operator/infrastructure/preview/testdata/000/out.yaml.golden index 5c6fb84..8699fef 100644 --- a/pkg/operator/infrastructure/preview/testdata/000/out.yaml.golden +++ b/pkg/operator/infrastructure/preview/testdata/000/out.yaml.golden @@ -307,15 +307,18 @@ Resources: - Type: A TTL: 60 + # + # AUTO GENERATED PREVIEW DEPLOYMENT + # - Service1d0fd508: + Service1D0FD508: Type: AWS::ECS::Service Properties: Cluster: !Ref Cluster - ServiceName: !Sub "${AWS::StackName}-lite-1d0fd508" + ServiceName: !Sub "${AWS::StackName}-lite-1D0FD508" DesiredCount: 1 LaunchType: "FARGATE" - TaskDefinition: !Ref TaskDefinition1d0fd508 + TaskDefinition: !Ref TaskDefinition1D0FD508 NetworkConfiguration: AwsvpcConfiguration: AssignPublicIp: "DISABLED" @@ -325,7 +328,7 @@ Resources: - Fn::ImportValue: !Sub "${NetworkStack}-PrivateSubnet1ID" - Fn::ImportValue: !Sub "${NetworkStack}-PrivateSubnet2ID" LoadBalancers: - - TargetGroupArn: !Ref TargetGroup1d0fd508 + - TargetGroupArn: !Ref TargetGroup1D0FD508 ContainerName: !Sub "${AWS::StackName}-lite" ContainerPort: !Ref LitePort Tags: @@ -336,10 +339,10 @@ Resources: DependsOn: - Cluster - TaskDefinition1d0fd508: + TaskDefinition1D0FD508: Type: AWS::ECS::TaskDefinition Properties: - Family: !Sub "${AWS::StackName}-lite-1d0fd508" + Family: !Sub "${AWS::StackName}-lite-1D0FD508" RequiresCompatibilities: - FARGATE Cpu: 512 @@ -369,12 +372,121 @@ Resources: - Key: "environment" Value: !Ref Environment - DomainRecord1d0fd508: + DomainRecord1D0FD508: + Type: AWS::Route53::RecordSet + Properties: + HostedZoneId: + Fn::ImportValue: !Sub "${NetworkStack}-EnvironmentHostedZoneId" + Name: !Sub "1D0FD508.lite.${Environment}.splits.org" + Type: A + AliasTarget: + DNSName: + Fn::ImportValue: !Sub "${FargateStack}-ApplicationLoadBalancerBaseDnsName" + HostedZoneId: + Fn::ImportValue: !Sub "${FargateStack}-ApplicationLoadBalancerCanonicalHostedZoneId" + + TargetGroup1D0FD508: + Type: AWS::ElasticLoadBalancingV2::TargetGroup + Properties: + Port: !Ref LitePort + Protocol: "HTTP" + VpcId: + Fn::ImportValue: !Sub "${NetworkStack}-VpcID" + TargetType: "ip" + HealthCheckPath: "/" + HealthCheckProtocol: "HTTP" + Matcher: + HttpCode: 200 + Tags: + - Key: "environment" + Value: !Ref Environment + + ListenerRule1D0FD508: + Type: AWS::ElasticLoadBalancingV2::ListenerRule + Properties: + Actions: + - Type: forward + TargetGroupArn: !Ref TargetGroup1D0FD508 + Conditions: + - Field: host-header + HostHeaderConfig: + Values: + - !Sub "1D0FD508.lite.${Environment}.splits.org" + ListenerArn: + Fn::ImportValue: !Sub "${FargateStack}-ApplicationLoadBalancerListenerArn" + Priority: 30 # Host header = 1D0FD508.lite.${Environment}.splits.org + + # + # AUTO GENERATED PREVIEW DEPLOYMENT + # + + ServiceF4436797: + Type: AWS::ECS::Service + Properties: + Cluster: !Ref Cluster + ServiceName: !Sub "${AWS::StackName}-lite-F4436797" + DesiredCount: 1 + LaunchType: "FARGATE" + TaskDefinition: !Ref TaskDefinitionF4436797 + NetworkConfiguration: + AwsvpcConfiguration: + AssignPublicIp: "DISABLED" + SecurityGroups: + - !Ref SecurityGroup + Subnets: + - Fn::ImportValue: !Sub "${NetworkStack}-PrivateSubnet1ID" + - Fn::ImportValue: !Sub "${NetworkStack}-PrivateSubnet2ID" + LoadBalancers: + - TargetGroupArn: !Ref TargetGroupF4436797 + ContainerName: !Sub "${AWS::StackName}-lite" + ContainerPort: !Ref LitePort + Tags: + - Key: "environment" + Value: !Ref Environment + - Key: "service" + Value: "splits-lite" + DependsOn: + - Cluster + + TaskDefinitionF4436797: + Type: AWS::ECS::TaskDefinition + Properties: + Family: !Sub "${AWS::StackName}-lite-F4436797" + RequiresCompatibilities: + - FARGATE + Cpu: 512 + Memory: 1024 + NetworkMode: awsvpc + ExecutionRoleArn: !GetAtt TaskExecutionRole.Arn + TaskRoleArn: !Sub "arn:aws:iam::${AWS::AccountId}:role/splits-ecs-role" + ContainerDefinitions: + - Name: !Sub "${AWS::StackName}-lite" + Image: !Sub "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/splits-lite:02b42b7ec63d4078767cb3b7cb0d34fde91b6237" + PortMappings: + - ContainerPort: !Ref LitePort + Protocol: "tcp" + Essential: true + Secrets: + - Name: ALCHEMY_API_KEY + ValueFrom: !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${Secret}:ALCHEMY_API_KEY::" + - Name: WALLETCONNECT_PROJECT_ID + ValueFrom: !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${Secret}:WALLETCONNECT_PROJECT_ID::" + LogConfiguration: + LogDriver: awslogs + Options: + awslogs-group: !Sub "/fargate/${AWS::StackName}/lite" + awslogs-region: !Ref AWS::Region + awslogs-stream-prefix: ecs + Tags: + - Key: "environment" + Value: !Ref Environment + + DomainRecordF4436797: Type: AWS::Route53::RecordSet Properties: HostedZoneId: Fn::ImportValue: !Sub "${NetworkStack}-EnvironmentHostedZoneId" - Name: !Sub "1d0fd508.lite.${Environment}.splits.org" + Name: !Sub "F4436797.lite.${Environment}.splits.org" Type: A AliasTarget: DNSName: @@ -382,7 +494,7 @@ Resources: HostedZoneId: Fn::ImportValue: !Sub "${FargateStack}-ApplicationLoadBalancerCanonicalHostedZoneId" - TargetGroup1d0fd508: + TargetGroupF4436797: Type: AWS::ElasticLoadBalancingV2::TargetGroup Properties: Port: !Ref LitePort @@ -398,17 +510,17 @@ Resources: - Key: "environment" Value: !Ref Environment - ListenerRule1d0fd508: + ListenerRuleF4436797: Type: AWS::ElasticLoadBalancingV2::ListenerRule Properties: Actions: - Type: forward - TargetGroupArn: !Ref TargetGroup1d0fd508 + TargetGroupArn: !Ref TargetGroupF4436797 Conditions: - Field: host-header HostHeaderConfig: Values: - - !Sub "1d0fd508.lite.${Environment}.splits.org" + - !Sub "F4436797.lite.${Environment}.splits.org" ListenerArn: Fn::ImportValue: !Sub "${FargateStack}-ApplicationLoadBalancerListenerArn" - Priority: 17 # Host header = 1d0fd508.lite.{environment}.splits.org + Priority: 31 # Host header = F4436797.lite.${Environment}.splits.org diff --git a/pkg/scanner/delete.go b/pkg/scanner/delete.go index 61c7e1a..c5147d2 100644 --- a/pkg/scanner/delete.go +++ b/pkg/scanner/delete.go @@ -7,8 +7,9 @@ import ( // Delete tries to drop the entire YAML block identified by the given key line, // e.g. " Service:". A new scanner configured without the found YAML block as -// input bytes is returned. -func (s *Scanner) Delete(key []byte) *Scanner { +// input bytes is returned. Optionally a substituion slice may be defined, which +// causes Delete to replace the matched block with the given structure. +func (s *Scanner) Delete(key []byte, sub ...byte) *Scanner { var buf *bufio.Scanner { buf = bufio.NewScanner(bytes.NewReader(s.inp)) @@ -16,6 +17,7 @@ func (s *Scanner) Delete(key []byte) *Scanner { var blo [][]byte var drp bool + var rep bool var end int var sta int for buf.Scan() { @@ -32,13 +34,16 @@ func (s *Scanner) Delete(key []byte) *Scanner { drp = false } - if bytes.Equal(lin, key) { + if bytes.HasPrefix(lin, key) { drp = true sta = spaces(lin) } if !drp { blo = append(blo, lin) + } else if !rep && sub != nil { + rep = true + blo = append(blo, sub) } } diff --git a/pkg/scanner/delete_test.go b/pkg/scanner/delete_test.go index 01eb2b2..a7ed77c 100644 --- a/pkg/scanner/delete_test.go +++ b/pkg/scanner/delete_test.go @@ -12,22 +12,42 @@ import ( func Test_Scanner_Delete(t *testing.T) { testCases := []struct { pre string + sub []byte }{ // Case 000 { pre: " Service:", + sub: nil, }, // Case 001 { pre: " TaskDefinition:", + sub: nil, }, // Case 002 { pre: "Resources:", + sub: nil, }, - // Case 003 + // Case 003, real production example { pre: " ServiceRegistries:", + sub: nil, + }, + // Case 004 + { + pre: " Image:", + sub: nil, + }, + // Case 005 + { + pre: " Image:", + sub: []byte(" Image: registry/image:tag"), + }, + // Case 006 + { + pre: " ContainerDefinitions:", + sub: []byte(" ContainerDefinitions:\n Foo: 1\n Bar: 2"), }, } @@ -60,7 +80,7 @@ func Test_Scanner_Delete(t *testing.T) { var res []byte { - res = sca.Delete([]byte(tc.pre)).Bytes() + res = sca.Delete([]byte(tc.pre), tc.sub...).Bytes() } if dif := cmp.Diff(bytes.TrimSpace(out), bytes.TrimSpace(res)); dif != "" { diff --git a/pkg/scanner/search.go b/pkg/scanner/search.go index 2eba010..3d93c56 100644 --- a/pkg/scanner/search.go +++ b/pkg/scanner/search.go @@ -34,7 +34,7 @@ func (s *Scanner) Search(key []byte) *Scanner { } if !fou { - fou = bytes.Equal(lin, key) + fou = bytes.HasPrefix(lin, key) sta = spaces(lin) } diff --git a/pkg/scanner/search_test.go b/pkg/scanner/search_test.go index 91094ea..6bbd90f 100644 --- a/pkg/scanner/search_test.go +++ b/pkg/scanner/search_test.go @@ -29,6 +29,10 @@ func Test_Scanner_Search(t *testing.T) { { key: " ServiceRegistries:", }, + // Case 004 + { + key: " Image:", + }, } for i, tc := range testCases { diff --git a/pkg/scanner/testdata/delete/004/inp.yaml.golden b/pkg/scanner/testdata/delete/004/inp.yaml.golden new file mode 100644 index 0000000..8197729 --- /dev/null +++ b/pkg/scanner/testdata/delete/004/inp.yaml.golden @@ -0,0 +1,32 @@ + TaskDefinition: + Type: AWS::ECS::TaskDefinition + Properties: + Family: !Sub "${AWS::StackName}-lite" + RequiresCompatibilities: + - FARGATE + Cpu: 512 + Memory: 1024 + NetworkMode: awsvpc + ExecutionRoleArn: !GetAtt TaskExecutionRole.Arn + TaskRoleArn: !Sub "arn:aws:iam::${AWS::AccountId}:role/splits-ecs-role" + ContainerDefinitions: + - Name: !Sub "${AWS::StackName}-lite" + Image: !Sub "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/splits-lite:${LiteVersion}" + PortMappings: + - ContainerPort: !Ref LitePort + Protocol: "tcp" + Essential: true + Secrets: + - Name: ALCHEMY_API_KEY + ValueFrom: !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${Secret}:ALCHEMY_API_KEY::" + - Name: WALLETCONNECT_PROJECT_ID + ValueFrom: !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${Secret}:WALLETCONNECT_PROJECT_ID::" + LogConfiguration: + LogDriver: awslogs + Options: + awslogs-group: !Sub "/fargate/${AWS::StackName}/lite" + awslogs-region: !Ref AWS::Region + awslogs-stream-prefix: ecs + Tags: + - Key: "environment" + Value: !Ref Environment diff --git a/pkg/scanner/testdata/delete/004/out.yaml.golden b/pkg/scanner/testdata/delete/004/out.yaml.golden new file mode 100644 index 0000000..31546d4 --- /dev/null +++ b/pkg/scanner/testdata/delete/004/out.yaml.golden @@ -0,0 +1,31 @@ + TaskDefinition: + Type: AWS::ECS::TaskDefinition + Properties: + Family: !Sub "${AWS::StackName}-lite" + RequiresCompatibilities: + - FARGATE + Cpu: 512 + Memory: 1024 + NetworkMode: awsvpc + ExecutionRoleArn: !GetAtt TaskExecutionRole.Arn + TaskRoleArn: !Sub "arn:aws:iam::${AWS::AccountId}:role/splits-ecs-role" + ContainerDefinitions: + - Name: !Sub "${AWS::StackName}-lite" + PortMappings: + - ContainerPort: !Ref LitePort + Protocol: "tcp" + Essential: true + Secrets: + - Name: ALCHEMY_API_KEY + ValueFrom: !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${Secret}:ALCHEMY_API_KEY::" + - Name: WALLETCONNECT_PROJECT_ID + ValueFrom: !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${Secret}:WALLETCONNECT_PROJECT_ID::" + LogConfiguration: + LogDriver: awslogs + Options: + awslogs-group: !Sub "/fargate/${AWS::StackName}/lite" + awslogs-region: !Ref AWS::Region + awslogs-stream-prefix: ecs + Tags: + - Key: "environment" + Value: !Ref Environment diff --git a/pkg/scanner/testdata/delete/005/inp.yaml.golden b/pkg/scanner/testdata/delete/005/inp.yaml.golden new file mode 100644 index 0000000..8197729 --- /dev/null +++ b/pkg/scanner/testdata/delete/005/inp.yaml.golden @@ -0,0 +1,32 @@ + TaskDefinition: + Type: AWS::ECS::TaskDefinition + Properties: + Family: !Sub "${AWS::StackName}-lite" + RequiresCompatibilities: + - FARGATE + Cpu: 512 + Memory: 1024 + NetworkMode: awsvpc + ExecutionRoleArn: !GetAtt TaskExecutionRole.Arn + TaskRoleArn: !Sub "arn:aws:iam::${AWS::AccountId}:role/splits-ecs-role" + ContainerDefinitions: + - Name: !Sub "${AWS::StackName}-lite" + Image: !Sub "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/splits-lite:${LiteVersion}" + PortMappings: + - ContainerPort: !Ref LitePort + Protocol: "tcp" + Essential: true + Secrets: + - Name: ALCHEMY_API_KEY + ValueFrom: !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${Secret}:ALCHEMY_API_KEY::" + - Name: WALLETCONNECT_PROJECT_ID + ValueFrom: !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${Secret}:WALLETCONNECT_PROJECT_ID::" + LogConfiguration: + LogDriver: awslogs + Options: + awslogs-group: !Sub "/fargate/${AWS::StackName}/lite" + awslogs-region: !Ref AWS::Region + awslogs-stream-prefix: ecs + Tags: + - Key: "environment" + Value: !Ref Environment diff --git a/pkg/scanner/testdata/delete/005/out.yaml.golden b/pkg/scanner/testdata/delete/005/out.yaml.golden new file mode 100644 index 0000000..760c846 --- /dev/null +++ b/pkg/scanner/testdata/delete/005/out.yaml.golden @@ -0,0 +1,32 @@ + TaskDefinition: + Type: AWS::ECS::TaskDefinition + Properties: + Family: !Sub "${AWS::StackName}-lite" + RequiresCompatibilities: + - FARGATE + Cpu: 512 + Memory: 1024 + NetworkMode: awsvpc + ExecutionRoleArn: !GetAtt TaskExecutionRole.Arn + TaskRoleArn: !Sub "arn:aws:iam::${AWS::AccountId}:role/splits-ecs-role" + ContainerDefinitions: + - Name: !Sub "${AWS::StackName}-lite" + Image: registry/image:tag + PortMappings: + - ContainerPort: !Ref LitePort + Protocol: "tcp" + Essential: true + Secrets: + - Name: ALCHEMY_API_KEY + ValueFrom: !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${Secret}:ALCHEMY_API_KEY::" + - Name: WALLETCONNECT_PROJECT_ID + ValueFrom: !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${Secret}:WALLETCONNECT_PROJECT_ID::" + LogConfiguration: + LogDriver: awslogs + Options: + awslogs-group: !Sub "/fargate/${AWS::StackName}/lite" + awslogs-region: !Ref AWS::Region + awslogs-stream-prefix: ecs + Tags: + - Key: "environment" + Value: !Ref Environment diff --git a/pkg/scanner/testdata/delete/006/inp.yaml.golden b/pkg/scanner/testdata/delete/006/inp.yaml.golden new file mode 100644 index 0000000..8197729 --- /dev/null +++ b/pkg/scanner/testdata/delete/006/inp.yaml.golden @@ -0,0 +1,32 @@ + TaskDefinition: + Type: AWS::ECS::TaskDefinition + Properties: + Family: !Sub "${AWS::StackName}-lite" + RequiresCompatibilities: + - FARGATE + Cpu: 512 + Memory: 1024 + NetworkMode: awsvpc + ExecutionRoleArn: !GetAtt TaskExecutionRole.Arn + TaskRoleArn: !Sub "arn:aws:iam::${AWS::AccountId}:role/splits-ecs-role" + ContainerDefinitions: + - Name: !Sub "${AWS::StackName}-lite" + Image: !Sub "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/splits-lite:${LiteVersion}" + PortMappings: + - ContainerPort: !Ref LitePort + Protocol: "tcp" + Essential: true + Secrets: + - Name: ALCHEMY_API_KEY + ValueFrom: !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${Secret}:ALCHEMY_API_KEY::" + - Name: WALLETCONNECT_PROJECT_ID + ValueFrom: !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${Secret}:WALLETCONNECT_PROJECT_ID::" + LogConfiguration: + LogDriver: awslogs + Options: + awslogs-group: !Sub "/fargate/${AWS::StackName}/lite" + awslogs-region: !Ref AWS::Region + awslogs-stream-prefix: ecs + Tags: + - Key: "environment" + Value: !Ref Environment diff --git a/pkg/scanner/testdata/delete/006/out.yaml.golden b/pkg/scanner/testdata/delete/006/out.yaml.golden new file mode 100644 index 0000000..a633e8a --- /dev/null +++ b/pkg/scanner/testdata/delete/006/out.yaml.golden @@ -0,0 +1,17 @@ + TaskDefinition: + Type: AWS::ECS::TaskDefinition + Properties: + Family: !Sub "${AWS::StackName}-lite" + RequiresCompatibilities: + - FARGATE + Cpu: 512 + Memory: 1024 + NetworkMode: awsvpc + ExecutionRoleArn: !GetAtt TaskExecutionRole.Arn + TaskRoleArn: !Sub "arn:aws:iam::${AWS::AccountId}:role/splits-ecs-role" + ContainerDefinitions: + Foo: 1 + Bar: 2 + Tags: + - Key: "environment" + Value: !Ref Environment diff --git a/pkg/scanner/testdata/search/004/inp.yaml.golden b/pkg/scanner/testdata/search/004/inp.yaml.golden new file mode 100644 index 0000000..8197729 --- /dev/null +++ b/pkg/scanner/testdata/search/004/inp.yaml.golden @@ -0,0 +1,32 @@ + TaskDefinition: + Type: AWS::ECS::TaskDefinition + Properties: + Family: !Sub "${AWS::StackName}-lite" + RequiresCompatibilities: + - FARGATE + Cpu: 512 + Memory: 1024 + NetworkMode: awsvpc + ExecutionRoleArn: !GetAtt TaskExecutionRole.Arn + TaskRoleArn: !Sub "arn:aws:iam::${AWS::AccountId}:role/splits-ecs-role" + ContainerDefinitions: + - Name: !Sub "${AWS::StackName}-lite" + Image: !Sub "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/splits-lite:${LiteVersion}" + PortMappings: + - ContainerPort: !Ref LitePort + Protocol: "tcp" + Essential: true + Secrets: + - Name: ALCHEMY_API_KEY + ValueFrom: !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${Secret}:ALCHEMY_API_KEY::" + - Name: WALLETCONNECT_PROJECT_ID + ValueFrom: !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${Secret}:WALLETCONNECT_PROJECT_ID::" + LogConfiguration: + LogDriver: awslogs + Options: + awslogs-group: !Sub "/fargate/${AWS::StackName}/lite" + awslogs-region: !Ref AWS::Region + awslogs-stream-prefix: ecs + Tags: + - Key: "environment" + Value: !Ref Environment diff --git a/pkg/scanner/testdata/search/004/out.yaml.golden b/pkg/scanner/testdata/search/004/out.yaml.golden new file mode 100644 index 0000000..5d5d32c --- /dev/null +++ b/pkg/scanner/testdata/search/004/out.yaml.golden @@ -0,0 +1 @@ + Image: !Sub "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/splits-lite:${LiteVersion}" From 1814c2027b7aef51e38e110c45ae5b4b79b6f856 Mon Sep 17 00:00:00 2001 From: xh3b4sd Date: Tue, 16 Sep 2025 17:02:44 +0200 Subject: [PATCH 05/13] fix --- pkg/cache/cache.go | 2 + pkg/cache/create.go | 10 ++ pkg/cache/delete.go | 1 + pkg/cache/object.go | 3 +- pkg/cache/previews.go | 18 +++ pkg/cache/releases.go | 1 + pkg/cache/update.go | 4 + pkg/operator/infrastructure/ensure.go | 31 +++- .../infrastructure/preview/preview.go | 30 ---- pkg/operator/reference/ensure.go | 4 +- pkg/operator/release/ensure.go | 30 +++- pkg/operator/release/release.go | 11 ++ pkg/operator/release/resolver/resolver.go | 4 + .../infrastructure => }/preview/error.go | 0 pkg/preview/expand.go | 86 ++++++++++++ pkg/preview/expand_test.go | 132 ++++++++++++++++++ .../infrastructure => }/preview/image.go | 0 .../infrastructure => }/preview/image_test.go | 2 +- pkg/preview/preview.go | 61 ++++++++ pkg/preview/priority.go | 21 +++ .../infrastructure => }/preview/render.go | 27 +++- .../preview/render_test.go | 6 + .../infrastructure => }/preview/resource.go | 0 .../preview/testdata/000/inp.yaml.golden | 2 +- .../preview/testdata/000/out.yaml.golden | 6 +- .../schema/release/deploy/preview/bool.go | 18 +++ pkg/release/schema/release/deploy/struct.go | 24 +++- .../schema/release/deploy/suspend/bool.go | 2 +- 28 files changed, 485 insertions(+), 51 deletions(-) create mode 100644 pkg/cache/previews.go delete mode 100644 pkg/operator/infrastructure/preview/preview.go rename pkg/{operator/infrastructure => }/preview/error.go (100%) create mode 100644 pkg/preview/expand.go create mode 100644 pkg/preview/expand_test.go rename pkg/{operator/infrastructure => }/preview/image.go (100%) rename pkg/{operator/infrastructure => }/preview/image_test.go (96%) create mode 100644 pkg/preview/preview.go create mode 100644 pkg/preview/priority.go rename pkg/{operator/infrastructure => }/preview/render.go (81%) rename pkg/{operator/infrastructure => }/preview/render_test.go (91%) rename pkg/{operator/infrastructure => }/preview/resource.go (100%) rename pkg/{operator/infrastructure => }/preview/testdata/000/inp.yaml.golden (99%) rename pkg/{operator/infrastructure => }/preview/testdata/000/out.yaml.golden (98%) create mode 100644 pkg/release/schema/release/deploy/preview/bool.go diff --git a/pkg/cache/cache.go b/pkg/cache/cache.go index 030c3db..10a339a 100644 --- a/pkg/cache/cache.go +++ b/pkg/cache/cache.go @@ -31,6 +31,7 @@ type Cache struct { inf []Object log logger.Interface mut sync.Mutex + pre []Object ser []Object } @@ -44,6 +45,7 @@ func New(c Config) *Cache { inf: nil, log: c.Log, mut: sync.Mutex{}, + pre: nil, ser: nil, } } diff --git a/pkg/cache/create.go b/pkg/cache/create.go index ecdbe78..c2f068f 100644 --- a/pkg/cache/create.go +++ b/pkg/cache/create.go @@ -20,6 +20,7 @@ func (c *Cache) Create(rel release.Slice) error { "docker", x.Docker.String(), "github", x.Github.String(), "deploy", x.Deploy.String(), + "preview", x.Deploy.Preview.String(), "provider", x.Provider.String(), ) @@ -44,6 +45,15 @@ func (c *Cache) Create(rel release.Slice) error { { c.inf = append(c.inf, obj) } + } else if bool(x.Deploy.Preview) { + { + obj.ind = len(c.pre) + obj.kin = Preview + } + + { + c.pre = append(c.pre, obj) + } } else { { obj.ind = len(c.ser) diff --git a/pkg/cache/delete.go b/pkg/cache/delete.go index 877159c..b52cbf5 100644 --- a/pkg/cache/delete.go +++ b/pkg/cache/delete.go @@ -13,6 +13,7 @@ func (c *Cache) Delete() { { c.inf = nil + c.pre = nil c.ser = nil } } diff --git a/pkg/cache/object.go b/pkg/cache/object.go index 9b41520..359754b 100644 --- a/pkg/cache/object.go +++ b/pkg/cache/object.go @@ -16,6 +16,7 @@ type kind string const ( Infrastructure kind = "infrastructure" + Preview kind = "preview" Service kind = "service" ) @@ -35,7 +36,7 @@ func (o Object) Name() string { return o.Release.Github.String() } - if o.kin == Service { + if o.kin == Preview || o.kin == Service { return o.Release.Docker.String() } diff --git a/pkg/cache/previews.go b/pkg/cache/previews.go new file mode 100644 index 0000000..318ed4d --- /dev/null +++ b/pkg/cache/previews.go @@ -0,0 +1,18 @@ +package cache + +func (c *Cache) Previews(doc string) []Object { + { + c.mut.Lock() + defer c.mut.Unlock() + } + + var lis []Object + + for _, x := range c.pre { + if x.Release.Docker.String() == doc { + lis = append(lis, x) + } + } + + return lis +} diff --git a/pkg/cache/releases.go b/pkg/cache/releases.go index af3ceb7..0153463 100644 --- a/pkg/cache/releases.go +++ b/pkg/cache/releases.go @@ -10,6 +10,7 @@ func (c *Cache) Releases() []Object { { lis = append(lis, c.inf...) + lis = append(lis, c.pre...) lis = append(lis, c.ser...) } diff --git a/pkg/cache/update.go b/pkg/cache/update.go index 2e33c86..0c4741a 100644 --- a/pkg/cache/update.go +++ b/pkg/cache/update.go @@ -10,6 +10,10 @@ func (c *Cache) Update(obj Object) { c.inf[obj.ind].Artifact = c.inf[obj.ind].Artifact.Merge(obj.Artifact) } + if obj.kin == Preview { + c.pre[obj.ind].Artifact = c.pre[obj.ind].Artifact.Merge(obj.Artifact) + } + if obj.kin == Service { c.ser[obj.ind].Artifact = c.ser[obj.ind].Artifact.Merge(obj.Artifact) } diff --git a/pkg/operator/infrastructure/ensure.go b/pkg/operator/infrastructure/ensure.go index 04cc447..3aed1ef 100644 --- a/pkg/operator/infrastructure/ensure.go +++ b/pkg/operator/infrastructure/ensure.go @@ -4,9 +4,11 @@ import ( "fmt" "io/fs" "path/filepath" + "strings" "github.com/0xSplits/kayron/pkg/cache" "github.com/0xSplits/kayron/pkg/constant" + "github.com/0xSplits/kayron/pkg/preview" "github.com/0xSplits/roghfs" "github.com/spf13/afero" "github.com/xh3b4sd/tracer" @@ -52,6 +54,8 @@ func (i *Infrastructure) Ensure() error { } } + // Skip everything that is not a YAML file. + var ext string { ext = filepath.Ext(fil.Name()) @@ -68,7 +72,32 @@ func (i *Infrastructure) Ensure() error { } } - // TODO somewhere here we need to inject the preview resources + // Before uploading our templates to S3, we have to inject any preview + // deployments configured for the service release that matches this + // particular template by file name. + // + // "lite.yaml" == Release.Docker.String() + // + + var pre *preview.Preview + { + pre = preview.New(preview.Config{ + Env: i.env, + Inp: byt, + }) + } + + var rep string + { + rep = strings.TrimSuffix(fil.Name(), ext) + } + + { + byt, err = pre.Render(i.cac.Previews(rep)) + if err != nil { + return tracer.Mask(err) + } + } { err = i.putObj(pat, byt) diff --git a/pkg/operator/infrastructure/preview/preview.go b/pkg/operator/infrastructure/preview/preview.go deleted file mode 100644 index 7a42c73..0000000 --- a/pkg/operator/infrastructure/preview/preview.go +++ /dev/null @@ -1,30 +0,0 @@ -package preview - -import ( - "fmt" - - "github.com/0xSplits/kayron/pkg/scanner" - "github.com/xh3b4sd/tracer" -) - -type Config struct { - Inp []byte -} - -type Preview struct { - inp []byte - sca *scanner.Scanner -} - -func New(c Config) *Preview { - if c.Inp == nil { - tracer.Panic(tracer.Mask(fmt.Errorf("%T.Inp must not be empty", c))) - } - - return &Preview{ - inp: c.Inp, - sca: scanner.New(scanner.Config{ - Inp: c.Inp, - }), - } -} diff --git a/pkg/operator/reference/ensure.go b/pkg/operator/reference/ensure.go index 2a3e70c..aac2964 100644 --- a/pkg/operator/reference/ensure.go +++ b/pkg/operator/reference/ensure.go @@ -7,8 +7,6 @@ import ( ) func (r *Reference) Ensure() error { - var err error - // Get the list of cached releases so that we can lookup their respective // artifact references concurrently, if necessary. This includes // infrastructure and service releases. @@ -51,7 +49,7 @@ func (r *Reference) Ensure() error { } { - err = parallel.Slice(rel, fnc) + err := parallel.Slice(rel, fnc) if err != nil { return tracer.Mask(err) } diff --git a/pkg/operator/release/ensure.go b/pkg/operator/release/ensure.go index a11064b..ae42e89 100644 --- a/pkg/operator/release/ensure.go +++ b/pkg/operator/release/ensure.go @@ -6,6 +6,7 @@ import ( "github.com/0xSplits/kayron/pkg/operator/release/resolver" "github.com/0xSplits/kayron/pkg/release/loader" "github.com/0xSplits/kayron/pkg/release/schema" + "github.com/0xSplits/kayron/pkg/release/schema/release" "github.com/0xSplits/roghfs" "github.com/spf13/afero" "github.com/xh3b4sd/tracer" @@ -110,11 +111,38 @@ func (r *Release) Ensure() error { } } + var rel release.Slice + + // TODO run in parallel + for _, x := range sch.Release { + // If this release has preview deployments enabled, then compute the preview + // releases and inject them into the new list of release definitions. + + if x.Deploy.Preview { + exp, err := r.pre.Expand(x) + if err != nil { + return tracer.Mask(err) + } + + { + rel = append(rel, exp...) + } + } + + // If this release has preview deployments disabled, then only track this + // main release in the new list of release definitions. + if !x.Deploy.Preview { + { + rel = append(rel, x) + } + } + } + // Initialize the cache for all configured releases regardless of their type. // Here we require exactly one infrastructure release to be provided. { - err = r.cac.Create(sch.Release) + err = r.cac.Create(rel) if err != nil { return tracer.Mask(err) } diff --git a/pkg/operator/release/release.go b/pkg/operator/release/release.go index 6916934..96c272d 100644 --- a/pkg/operator/release/release.go +++ b/pkg/operator/release/release.go @@ -11,6 +11,7 @@ import ( "github.com/0xSplits/kayron/pkg/envvar" "github.com/0xSplits/kayron/pkg/operator/release/canceler" "github.com/0xSplits/kayron/pkg/operator/release/resolver" + "github.com/0xSplits/kayron/pkg/preview" "github.com/0xSplits/kayron/pkg/stack" "github.com/0xSplits/roghfs" "github.com/aws/aws-sdk-go-v2/aws" @@ -34,6 +35,7 @@ type Release struct { git *github.Client log logger.Interface own string + pre *preview.Preview rep string res resolver.Interface sta stack.Interface @@ -81,6 +83,14 @@ func New(c Config) *Release { }) } + var pre *preview.Preview + { + pre = preview.New(preview.Config{ + Env: c.Env, + Inp: []byte{}, + }) + } + var res resolver.Interface { res = resolver.New(resolver.Config{ @@ -97,6 +107,7 @@ func New(c Config) *Release { git: git, log: c.Log, own: own, + pre: pre, rep: rep, res: res, sta: c.Sta, diff --git a/pkg/operator/release/resolver/resolver.go b/pkg/operator/release/resolver/resolver.go index 1bdf6c5..34f914c 100644 --- a/pkg/operator/release/resolver/resolver.go +++ b/pkg/operator/release/resolver/resolver.go @@ -1,3 +1,7 @@ +// Package resolver tries to provide the environment specific Git reference for +// the release source repository. This Git reference tells us which version of +// our service releases to consider, based on the environment Kayron is running +// in. package resolver import ( diff --git a/pkg/operator/infrastructure/preview/error.go b/pkg/preview/error.go similarity index 100% rename from pkg/operator/infrastructure/preview/error.go rename to pkg/preview/error.go diff --git a/pkg/preview/expand.go b/pkg/preview/expand.go new file mode 100644 index 0000000..f906685 --- /dev/null +++ b/pkg/preview/expand.go @@ -0,0 +1,86 @@ +package preview + +import ( + "context" + "sort" + + "github.com/0xSplits/kayron/pkg/release/schema/release" + "github.com/0xSplits/kayron/pkg/release/schema/release/deploy" + "github.com/0xSplits/kayron/pkg/release/schema/release/deploy/branch" + "github.com/0xSplits/kayron/pkg/release/schema/release/deploy/preview" + "github.com/google/go-github/v73/github" + "github.com/xh3b4sd/tracer" +) + +func (p *Preview) Expand(rel release.Struct) (release.Slice, error) { + var err error + + opt := &github.PullRequestListOptions{ + State: "open", + ListOptions: github.ListOptions{ + PerPage: 100, + }, + } + + var pul []*github.PullRequest + { + pul, _, err = p.git.PullRequests.List(context.Background(), p.own, rel.Github.String(), opt) + if err != nil { + return nil, tracer.Mask(err) + } + } + + var lis release.Slice + { + lis = expand(rel, pul) + } + + return lis, nil +} + +func expand(rel release.Struct, pul []*github.PullRequest) release.Slice { + // Sort pull requests from oldest to newest, so that new pull requests do not + // change the order of pull request specific preview deployments. This is + // relevant because the order of preview releases created below will define + // the priority settings of the ALB's listener rules. + + sort.Slice(pul, func(i, j int) bool { + return pul[i].GetCreatedAt().Before(pul[j].GetCreatedAt().Time) + }) + + // Mark the expanded service release as non-preview. The Deploy.Preview + // flag acts as a signal to expand our release definitions internally. + // Once expanded, we redefine the purpose of this preview flag to maintain + // our understanding of how to deploy "real" service releases. In other + // words, we turn one release into many, while muting the one that + // instructed the many for the preview mechanism. + + { + rel.Deploy.Preview = preview.Bool(false) + } + + var lis release.Slice + { + lis = append(lis, rel) + } + + for _, x := range pul { + var pre release.Struct + { + pre = rel + } + + { + pre.Deploy = deploy.Struct{ + Branch: branch.String(x.GetHead().GetRef()), + Preview: preview.Bool(true), + } + } + + { + lis = append(lis, pre) + } + } + + return lis +} diff --git a/pkg/preview/expand_test.go b/pkg/preview/expand_test.go new file mode 100644 index 0000000..087214f --- /dev/null +++ b/pkg/preview/expand_test.go @@ -0,0 +1,132 @@ +package preview + +import ( + "fmt" + "testing" + "time" + + "github.com/0xSplits/kayron/pkg/release/schema/release" + "github.com/0xSplits/kayron/pkg/release/schema/release/deploy" + "github.com/0xSplits/kayron/pkg/release/schema/release/deploy/branch" + "github.com/0xSplits/kayron/pkg/release/schema/release/deploy/preview" + "github.com/0xSplits/kayron/pkg/release/schema/release/docker" + "github.com/google/go-cmp/cmp" + "github.com/google/go-github/v73/github" +) + +func Test_Preview_Expand(t *testing.T) { + testCases := []struct { + rel release.Struct + pul []*github.PullRequest + exp release.Slice + }{ + // Case 000 + { + rel: release.Struct{ + Deploy: deploy.Struct{ + Branch: branch.String("main"), + Preview: preview.Bool(true), + }, + Docker: docker.String("lite"), + }, + pul: []*github.PullRequest{ + {CreatedAt: tesTim(3), Head: tesBra("b/3")}, + {CreatedAt: tesTim(5), Head: tesBra("b/5")}, + }, + exp: release.Slice{ + { + Deploy: deploy.Struct{ + Branch: branch.String("main"), + Preview: preview.Bool(false), + }, + Docker: docker.String("lite"), + }, + { + Deploy: deploy.Struct{ + Branch: branch.String("b/3"), + Preview: preview.Bool(true), + }, + Docker: docker.String("lite"), + }, + { + Deploy: deploy.Struct{ + Branch: branch.String("b/5"), + Preview: preview.Bool(true), + }, + Docker: docker.String("lite"), + }, + }, + }, + // Case 001 + { + rel: release.Struct{ + Deploy: deploy.Struct{ + Branch: branch.String("main"), + Preview: preview.Bool(true), + }, + Docker: docker.String("lite"), + }, + pul: []*github.PullRequest{ + {CreatedAt: tesTim(3), Head: tesBra("b/3")}, + {CreatedAt: tesTim(5), Head: tesBra("b/5")}, + {CreatedAt: tesTim(7), Head: tesBra("b/7")}, + {CreatedAt: tesTim(9), Head: tesBra("b/9")}, + }, + exp: release.Slice{ + { + Deploy: deploy.Struct{ + Branch: branch.String("main"), + Preview: preview.Bool(false), + }, + Docker: docker.String("lite"), + }, + { + Deploy: deploy.Struct{ + Branch: branch.String("b/3"), + Preview: preview.Bool(true), + }, + Docker: docker.String("lite"), + }, + { + Deploy: deploy.Struct{ + Branch: branch.String("b/5"), + Preview: preview.Bool(true), + }, + Docker: docker.String("lite"), + }, + { + Deploy: deploy.Struct{ + Branch: branch.String("b/7"), + Preview: preview.Bool(true), + }, + Docker: docker.String("lite"), + }, + { + Deploy: deploy.Struct{ + Branch: branch.String("b/9"), + Preview: preview.Bool(true), + }, + Docker: docker.String("lite"), + }, + }, + }, + } + + for i, tc := range testCases { + t.Run(fmt.Sprintf("%03d", i), func(t *testing.T) { + exp := expand(tc.rel, tc.pul) + + if dif := cmp.Diff(tc.exp, exp); dif != "" { + t.Fatalf("-expected +actual:\n%s", dif) + } + }) + } +} + +func tesBra(nam string) *github.PullRequestBranch { + return &github.PullRequestBranch{Ref: github.Ptr(nam)} +} + +func tesTim(sec int64) *github.Timestamp { + return &github.Timestamp{Time: time.Unix(sec, 0)} +} diff --git a/pkg/operator/infrastructure/preview/image.go b/pkg/preview/image.go similarity index 100% rename from pkg/operator/infrastructure/preview/image.go rename to pkg/preview/image.go diff --git a/pkg/operator/infrastructure/preview/image_test.go b/pkg/preview/image_test.go similarity index 96% rename from pkg/operator/infrastructure/preview/image_test.go rename to pkg/preview/image_test.go index 1f41145..78d8f79 100644 --- a/pkg/operator/infrastructure/preview/image_test.go +++ b/pkg/preview/image_test.go @@ -7,7 +7,7 @@ import ( "github.com/google/go-cmp/cmp" ) -func Test_Operator_Infrastructure_Preview_Image(t *testing.T) { +func Test_Preview_repIma(t *testing.T) { testCases := []struct { lin []byte tag []byte diff --git a/pkg/preview/preview.go b/pkg/preview/preview.go new file mode 100644 index 0000000..9ecf79f --- /dev/null +++ b/pkg/preview/preview.go @@ -0,0 +1,61 @@ +package preview + +import ( + "fmt" + + "github.com/0xSplits/kayron/pkg/envvar" + "github.com/0xSplits/kayron/pkg/scanner" + "github.com/0xSplits/roghfs" + "github.com/google/go-github/v73/github" + "github.com/xh3b4sd/tracer" +) + +type Config struct { + Env envvar.Env + Inp []byte +} + +type Preview struct { + git *github.Client + inp []byte + own string + sca *scanner.Scanner +} + +func New(c Config) *Preview { + if c.Env.Environment == "" { + tracer.Panic(tracer.Mask(fmt.Errorf("%T.Env must not be empty", c))) + } + if c.Inp == nil { + tracer.Panic(tracer.Mask(fmt.Errorf("%T.Inp must not be empty", c))) + } + + var err error + + var git *github.Client + { + git = github.NewClient(nil).WithAuthToken(c.Env.GithubToken) + } + + var own string + { + own, _, err = roghfs.Parse(c.Env.ReleaseSource) + if err != nil { + tracer.Panic(tracer.Mask(err)) + } + } + + var sca *scanner.Scanner + { + sca = scanner.New(scanner.Config{ + Inp: c.Inp, + }) + } + + return &Preview{ + git: git, + inp: c.Inp, + own: own, + sca: sca, + } +} diff --git a/pkg/preview/priority.go b/pkg/preview/priority.go new file mode 100644 index 0000000..2f547e4 --- /dev/null +++ b/pkg/preview/priority.go @@ -0,0 +1,21 @@ +package preview + +import ( + "bytes" + + "github.com/goccy/go-yaml" + "github.com/xh3b4sd/tracer" +) + +func lisPri(lin []byte) (int, error) { + var pri map[string]int + + { + err := yaml.Unmarshal(bytes.TrimSpace(lin), &pri) + if err != nil { + return 0, tracer.Mask(err) + } + } + + return pri["Priority"], nil +} diff --git a/pkg/operator/infrastructure/preview/render.go b/pkg/preview/render.go similarity index 81% rename from pkg/operator/infrastructure/preview/render.go rename to pkg/preview/render.go index 765ced6..6226369 100644 --- a/pkg/operator/infrastructure/preview/render.go +++ b/pkg/preview/render.go @@ -20,9 +20,14 @@ var ( ) ) -func (p *Preview) Render(art []cache.Object) ([]byte, error) { +func (p *Preview) Render(pre []cache.Object) ([]byte, error) { var err error + // TODO write test + if len(pre) == 0 { + return p.inp, nil + } + var res Resource { res = Resource{ @@ -39,12 +44,18 @@ func (p *Preview) Render(art []cache.Object) ([]byte, error) { out = append(p.inp, '\n') } + // Derive the base of our listener rule priority range from the provided + // template defining a AWS::ElasticLoadBalancingV2::ListenerRule resource. + var pri int { - pri = 30 + pri, err = lisPri(res.Lis.Search([]byte(" Priority:")).Bytes()) + if err != nil { + return nil, tracer.Mask(err) + } } - for _, x := range art { + for _, x := range pre { var hsh hash.Hash { hsh = hash.New(x.Release.Deploy.Branch.String()) @@ -63,19 +74,23 @@ func (p *Preview) Render(art []cache.Object) ([]byte, error) { } } + // Increment the listener rule priority per preview release. This must be + // done before the call to render(), because the base priority resolved + // above is already taken by the main release defining our preview + // deployments. + { - out = append(out, p.render(res, dom, pri, hsh, ima)...) + pri++ } { - pri++ + out = append(out, p.render(res, dom, pri, hsh, ima)...) } } return out, nil } -// TODO scanner needs Replace and Delete func (p *Preview) render(res Resource, dom string, pri int, hsh hash.Hash, ima []byte) []byte { { res.Ser = res.Ser.Append([]byte(" Service:"), hsh.Hsh) diff --git a/pkg/operator/infrastructure/preview/render_test.go b/pkg/preview/render_test.go similarity index 91% rename from pkg/operator/infrastructure/preview/render_test.go rename to pkg/preview/render_test.go index 9da48bd..492f4cd 100644 --- a/pkg/operator/infrastructure/preview/render_test.go +++ b/pkg/preview/render_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/0xSplits/kayron/pkg/cache" + "github.com/0xSplits/kayron/pkg/envvar" "github.com/0xSplits/kayron/pkg/release/artifact" "github.com/0xSplits/kayron/pkg/release/artifact/reference" "github.com/0xSplits/kayron/pkg/release/schema/release" @@ -76,6 +77,11 @@ func Test_Operator_Infrastructure_Preview_Render(t *testing.T) { var pre *Preview { pre = New(Config{ + Env: envvar.Env{ + Environment: "testing", + GithubToken: "foo", + ReleaseSource: "https://github.com/0xSplits/releases", + }, Inp: inp, }) } diff --git a/pkg/operator/infrastructure/preview/resource.go b/pkg/preview/resource.go similarity index 100% rename from pkg/operator/infrastructure/preview/resource.go rename to pkg/preview/resource.go diff --git a/pkg/operator/infrastructure/preview/testdata/000/inp.yaml.golden b/pkg/preview/testdata/000/inp.yaml.golden similarity index 99% rename from pkg/operator/infrastructure/preview/testdata/000/inp.yaml.golden rename to pkg/preview/testdata/000/inp.yaml.golden index e8ecafe..d550cbb 100644 --- a/pkg/operator/infrastructure/preview/testdata/000/inp.yaml.golden +++ b/pkg/preview/testdata/000/inp.yaml.golden @@ -193,7 +193,7 @@ Resources: - !Ref LiteDomain ListenerArn: Fn::ImportValue: !Sub "${FargateStack}-ApplicationLoadBalancerListenerArn" - Priority: 12 # Host header = lite.{environment}.splits.org + Priority: 12000 # Host header = lite.${Environment}.splits.org ListenerCertificate: Type: AWS::ElasticLoadBalancingV2::ListenerCertificate diff --git a/pkg/operator/infrastructure/preview/testdata/000/out.yaml.golden b/pkg/preview/testdata/000/out.yaml.golden similarity index 98% rename from pkg/operator/infrastructure/preview/testdata/000/out.yaml.golden rename to pkg/preview/testdata/000/out.yaml.golden index 8699fef..cf3413c 100644 --- a/pkg/operator/infrastructure/preview/testdata/000/out.yaml.golden +++ b/pkg/preview/testdata/000/out.yaml.golden @@ -193,7 +193,7 @@ Resources: - !Ref LiteDomain ListenerArn: Fn::ImportValue: !Sub "${FargateStack}-ApplicationLoadBalancerListenerArn" - Priority: 12 # Host header = lite.{environment}.splits.org + Priority: 12000 # Host header = lite.${Environment}.splits.org ListenerCertificate: Type: AWS::ElasticLoadBalancingV2::ListenerCertificate @@ -414,7 +414,7 @@ Resources: - !Sub "1D0FD508.lite.${Environment}.splits.org" ListenerArn: Fn::ImportValue: !Sub "${FargateStack}-ApplicationLoadBalancerListenerArn" - Priority: 30 # Host header = 1D0FD508.lite.${Environment}.splits.org + Priority: 12001 # Host header = 1D0FD508.lite.${Environment}.splits.org # # AUTO GENERATED PREVIEW DEPLOYMENT @@ -523,4 +523,4 @@ Resources: - !Sub "F4436797.lite.${Environment}.splits.org" ListenerArn: Fn::ImportValue: !Sub "${FargateStack}-ApplicationLoadBalancerListenerArn" - Priority: 31 # Host header = F4436797.lite.${Environment}.splits.org + Priority: 12002 # Host header = F4436797.lite.${Environment}.splits.org diff --git a/pkg/release/schema/release/deploy/preview/bool.go b/pkg/release/schema/release/deploy/preview/bool.go new file mode 100644 index 0000000..d1d3801 --- /dev/null +++ b/pkg/release/schema/release/deploy/preview/bool.go @@ -0,0 +1,18 @@ +package preview + +import "strconv" + +// Bool enables preview deployments for this service. +type Bool bool + +func (b Bool) Empty() bool { + return !bool(b) +} + +func (b Bool) String() string { + return strconv.FormatBool(bool(b)) +} + +func (b Bool) Verify() error { + return nil +} diff --git a/pkg/release/schema/release/deploy/struct.go b/pkg/release/schema/release/deploy/struct.go index 96566a7..b337943 100644 --- a/pkg/release/schema/release/deploy/struct.go +++ b/pkg/release/schema/release/deploy/struct.go @@ -2,6 +2,7 @@ package deploy import ( "github.com/0xSplits/kayron/pkg/release/schema/release/deploy/branch" + "github.com/0xSplits/kayron/pkg/release/schema/release/deploy/preview" "github.com/0xSplits/kayron/pkg/release/schema/release/deploy/release" "github.com/0xSplits/kayron/pkg/release/schema/release/deploy/suspend" "github.com/0xSplits/kayron/pkg/release/schema/release/deploy/webhook" @@ -9,16 +10,20 @@ import ( ) // Struct defines exactly one mutually exclusive declaration of either Branch, -// Release, Suspend or Webhook as required deployment instruction. +// Release, Suspend or Webhook as required deployment instruction. Struct may +// also define Preview as a testing environment only deployment mechanism for +// pull requests, which does not influence the main deployment strategy +// configuration mentioned above. type Struct struct { Branch branch.String `yaml:"branch,omitempty"` + Preview preview.Bool `yaml:"preview,omitempty"` Release release.String `yaml:"release,omitempty"` Suspend suspend.Bool `yaml:"suspend,omitempty"` Webhook webhook.Slice `yaml:"webhook,omitempty"` } func (s Struct) Empty() bool { - return s.Branch.Empty() && s.Release.Empty() && s.Suspend.Empty() && s.Webhook.Empty() + return s.Branch.Empty() && s.Preview.Empty() && s.Release.Empty() && s.Suspend.Empty() && s.Webhook.Empty() } func (s Struct) String() string { @@ -26,6 +31,10 @@ func (s Struct) String() string { return s.Branch.String() } + // Note that Struct.Preview is not a deployment strategy in all environments, + // so Struct.Preview does not contribute to the name/string representation of + // this deployment strategy. + if !s.Release.Empty() { return s.Release.String() } @@ -42,7 +51,9 @@ func (s Struct) String() string { } func (s Struct) Verify() error { - // Reject deployment configurations that define more than one strategy. + // Reject deployment configurations that define more than one strategy. Note + // that s.Preview is not a deployment strategy and is therefore not considered + // for this check. { lis := enabled(s.Branch, s.Release, s.Suspend, s.Webhook) if len(lis) > 1 { @@ -57,6 +68,13 @@ func (s Struct) Verify() error { } } + if !s.Preview.Empty() { + err := s.Preview.Verify() + if err != nil { + return tracer.Mask(err) + } + } + if !s.Release.Empty() { err := s.Release.Verify() if err != nil { diff --git a/pkg/release/schema/release/deploy/suspend/bool.go b/pkg/release/schema/release/deploy/suspend/bool.go index a16b006..9f92dc5 100644 --- a/pkg/release/schema/release/deploy/suspend/bool.go +++ b/pkg/release/schema/release/deploy/suspend/bool.go @@ -2,7 +2,7 @@ package suspend import "strconv" -// Bool disables any further reconciliation of this service indefinitely. +// Bool disables any further reconciliation of this release indefinitely. type Bool bool func (b Bool) Empty() bool { From eb9f56e195f25218499897a6c6d79c62261faba5 Mon Sep 17 00:00:00 2001 From: xh3b4sd Date: Wed, 17 Sep 2025 16:29:56 +0200 Subject: [PATCH 06/13] fix --- README.md | 240 ++++++++++++------ pkg/cache/cache.go | 2 - pkg/cache/create.go | 9 - pkg/cache/delete.go | 1 - pkg/cache/object.go | 3 +- pkg/cache/previews.go | 6 +- pkg/cache/releases.go | 1 - pkg/cache/services.go | 2 + pkg/cache/update.go | 4 - pkg/hash/hash.go | 12 +- pkg/operator/container/cache.go | 7 +- pkg/operator/container/image.go | 4 + pkg/operator/container/task.go | 18 +- pkg/operator/infrastructure/ensure.go | 60 +++-- pkg/operator/policy/ensure.go | 1 + pkg/operator/reference/ensure.go | 21 +- pkg/operator/registry/ensure.go | 17 +- pkg/operator/release/ensure.go | 6 + pkg/preview/domain.go | 22 ++ pkg/preview/expand.go | 62 ++++- pkg/preview/expand_test.go | 24 ++ pkg/preview/render.go | 46 ++-- pkg/preview/render_test.go | 10 +- pkg/preview/tags.go | 26 ++ pkg/preview/testdata/000/out.yaml.golden | 8 +- pkg/release/schema/release/deploy/struct.go | 10 +- pkg/release/schema/release/labels/struct.go | 13 +- pkg/scanner/search.go | 2 +- pkg/scanner/search_test.go | 4 + .../testdata/search/005/inp.yaml.golden | 32 +++ .../testdata/search/005/out.yaml.golden | 5 + 31 files changed, 502 insertions(+), 176 deletions(-) create mode 100644 pkg/preview/domain.go create mode 100644 pkg/preview/tags.go create mode 100644 pkg/scanner/testdata/search/005/inp.yaml.golden create mode 100644 pkg/scanner/testdata/search/005/out.yaml.golden diff --git a/README.md b/README.md index 7925afb..e9a4e73 100644 --- a/README.md +++ b/README.md @@ -147,167 +147,244 @@ go test -tags=integration ./pkg/operator -v -race -run Test_Operator_Integration ```yaml === RUN Test_Operator_Integration { - "time": "2025-08-21 18:19:32", + "time": "2025-09-17 14:24:33", "level": "debug", "message": "resetting operator cache", "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/cache/delete.go:9" } { - "time": "2025-08-21 18:19:34", + "time": "2025-09-17 14:24:35", "level": "debug", "message": "resolved ref for github repository", "environment": "testing", - "ref": "2fd5cb440fe5e6b9ff205ea94c07a77a331fc79e", + "ref": "176ffefa272b210eb3be269887d665a682dbd548", "repository": "https://github.com/0xSplits/releases", - "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/release/ensure.go:65" + "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/release/ensure.go:66" } { - "time": "2025-08-21 18:19:35", + "time": "2025-09-17 14:24:37", "level": "debug", "message": "caching release artifact", - "deploy": "v0.1.5", + "deploy": "branch=preview", "github": "infrastructure", + "preview": "false", "provider": "cloudformation", "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/cache/create.go:17" } { - "time": "2025-08-21 18:19:35", + "time": "2025-09-17 14:24:37", "level": "debug", "message": "caching release artifact", - "deploy": "v0.1.9", + "deploy": "release=v0.1.1", + "docker": "splits-lite", + "github": "splits-lite", + "preview": "false", + "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/cache/create.go:17" +} +{ + "time": "2025-09-17 14:24:37", + "level": "debug", + "message": "caching release artifact", + "deploy": "branch=fancy-feature-branch", + "docker": "splits-lite", + "github": "splits-lite", + "preview": "true", + "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/cache/create.go:17" +} +{ + "time": "2025-09-17 14:24:37", + "level": "debug", + "message": "caching release artifact", + "deploy": "branch=preview", "docker": "kayron", "github": "kayron", + "preview": "false", "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/cache/create.go:17" } { - "time": "2025-08-21 18:19:35", + "time": "2025-09-17 14:24:37", "level": "debug", "message": "caching release artifact", - "deploy": "v0.1.18", + "deploy": "release=v0.2.2", "docker": "specta", "github": "specta", + "preview": "false", "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/cache/create.go:17" } { - "time": "2025-08-21 18:19:35", + "time": "2025-09-17 14:24:37", "level": "debug", "message": "instrumented worker handler", "handler": "release", - "latency": "2.2552785s", + "latency": "3.93228675s", "success": "true", - "caller": "/Users/xh3b4sd/project/0xSplits/workit/handler/metrics/ensure.go:55" + "caller": "/Users/xh3b4sd/go/pkg/mod/github.com/0x!splits/workit@v0.6.0/handler/metrics/ensure.go:55" } { - "time": "2025-08-21 18:19:35", + "time": "2025-09-17 14:24:37", "level": "debug", "message": "caching desired state", - "desired": "v0.1.18", + "desired": "v0.2.2", "github": "specta", - "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/reference/ensure.go:35" + "preview": "false", + "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/reference/ensure.go:45" +} +{ + "time": "2025-09-17 14:24:37", + "level": "debug", + "message": "caching current state", + "current": "3c113413a19f82f906f45ba22c2cd17bb7a62682", + "github": "infrastructure", + "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/template/ensure.go:35" } { - "time": "2025-08-21 18:19:35", + "time": "2025-09-17 14:24:37", "level": "debug", "message": "caching desired state", - "desired": "v0.1.9", - "github": "kayron", - "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/reference/ensure.go:35" + "desired": "v0.1.1", + "github": "splits-lite", + "preview": "false", + "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/reference/ensure.go:45" } { - "time": "2025-08-21 18:19:35", + "time": "2025-09-17 14:24:37", + "level": "debug", + "message": "instrumented worker handler", + "handler": "template", + "latency": "238.584µs", + "success": "true", + "caller": "/Users/xh3b4sd/go/pkg/mod/github.com/0x!splits/workit@v0.6.0/handler/metrics/ensure.go:55" +} +{ + "time": "2025-09-17 14:24:37", "level": "debug", "message": "caching desired state", - "desired": "v0.1.5", + "desired": "d5fa88afd502edc9052f89c956618b2cb567d984", "github": "infrastructure", - "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/reference/ensure.go:35" + "preview": "false", + "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/reference/ensure.go:45" } { - "time": "2025-08-21 18:19:35", + "time": "2025-09-17 14:24:37", "level": "debug", - "message": "caching current state", - "current": "v0.1.5", - "github": "infrastructure", - "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/template/ensure.go:35" + "message": "caching desired state", + "desired": "1814c2027b7aef51e38e110c45ae5b4b79b6f856", + "github": "kayron", + "preview": "false", + "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/reference/ensure.go:45" } { - "time": "2025-08-21 18:19:35", + "time": "2025-09-17 14:24:37", + "level": "debug", + "message": "caching desired state", + "desired": "bc7891268e44f62e0aebbe339c0850b61d52c417", + "github": "splits-lite", + "preview": "true", + "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/reference/ensure.go:45" +} +{ + "time": "2025-09-17 14:24:37", "level": "debug", "message": "instrumented worker handler", "handler": "reference", - "latency": "494.083µs", + "latency": "304.052167ms", "success": "true", - "caller": "/Users/xh3b4sd/project/0xSplits/workit/handler/metrics/ensure.go:55" + "caller": "/Users/xh3b4sd/go/pkg/mod/github.com/0x!splits/workit@v0.6.0/handler/metrics/ensure.go:55" } { - "time": "2025-08-21 18:19:35", + "time": "2025-09-17 14:24:39", "level": "debug", - "message": "instrumented worker handler", - "handler": "template", - "latency": "741.125µs", - "success": "true", - "caller": "/Users/xh3b4sd/project/0xSplits/workit/handler/metrics/ensure.go:55" + "message": "caching current state", + "current": "v0.1.1", + "docker": "splits-lite", + "preview": "false", + "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/container/cache.go:10" } { - "time": "2025-08-21 18:19:37", + "time": "2025-09-17 14:24:39", "level": "debug", "message": "caching current state", - "current": "v0.1.9", + "current": "''", + "docker": "splits-lite", + "preview": "true", + "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/container/cache.go:10" +} +{ + "time": "2025-09-17 14:24:39", + "level": "debug", + "message": "caching current state", + "current": "1814c2027b7aef51e38e110c45ae5b4b79b6f856", "docker": "kayron", + "preview": "false", "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/container/cache.go:10" } { - "time": "2025-08-21 18:19:37", + "time": "2025-09-17 14:24:39", "level": "debug", "message": "caching current state", - "current": "v0.1.18", + "current": "v0.2.2", "docker": "specta", + "preview": "false", "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/container/cache.go:10" } { - "time": "2025-08-21 18:19:37", + "time": "2025-09-17 14:24:39", "level": "debug", "message": "instrumented worker handler", "handler": "container", - "latency": "2.0316385s", + "latency": "1.884508333s", "success": "true", - "caller": "/Users/xh3b4sd/project/0xSplits/workit/handler/metrics/ensure.go:55" + "caller": "/Users/xh3b4sd/go/pkg/mod/github.com/0x!splits/workit@v0.6.0/handler/metrics/ensure.go:55" +} +{ + "time": "2025-09-17 14:24:40", + "level": "debug", + "message": "executed image check", + "exists": "true", + "image": "splits-lite", + "preview": "true", + "tag": "bc7891268e44f62e0aebbe339c0850b61d52c417", + "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/registry/ensure.go:45" } { - "time": "2025-08-21 18:19:37", + "time": "2025-09-17 14:24:40", "level": "debug", "message": "instrumented worker handler", "handler": "registry", - "latency": "55.416µs", + "latency": "1.037679042s", "success": "true", - "caller": "/Users/xh3b4sd/project/0xSplits/workit/handler/metrics/ensure.go:55" + "caller": "/Users/xh3b4sd/go/pkg/mod/github.com/0x!splits/workit@v0.6.0/handler/metrics/ensure.go:55" } { - "time": "2025-08-21 18:19:37", + "time": "2025-09-17 14:24:40", "level": "info", "message": "continuing reconciliation loop", + "preview": "false", "reason": "detected state drift", - "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/policy/ensure.go:47" + "release": "infrastructure", + "version": "d5fa88afd502edc9052f89c956618b2cb567d984", + "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/policy/ensure.go:62" } { - "time": "2025-08-21 18:19:37", + "time": "2025-09-17 14:24:40", "level": "debug", "message": "instrumented worker handler", "handler": "policy", - "latency": "112.334µs", + "latency": "277.75µs", "success": "true", - "caller": "/Users/xh3b4sd/project/0xSplits/workit/handler/metrics/ensure.go:55" + "caller": "/Users/xh3b4sd/go/pkg/mod/github.com/0x!splits/workit@v0.6.0/handler/metrics/ensure.go:55" } { - "time": "2025-08-21 18:19:37", + "time": "2025-09-17 14:24:40", "level": "debug", "message": "resolved ref for github repository", "environment": "testing", - "ref": "v0.1.5", + "ref": "d5fa88afd502edc9052f89c956618b2cb567d984", "repository": "https://github.com/0xSplits/infrastructure", - "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/infrastructure/ensure.go:22" + "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/infrastructure/ensure.go:24" } { - "time": "2025-08-21 18:19:37", + "time": "2025-09-17 14:24:40", "level": "debug", "message": "uploading cloudformation template", "bucket": "splits-cf-templates", @@ -315,7 +392,7 @@ go test -tags=integration ./pkg/operator -v -race -run Test_Operator_Integration "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/infrastructure/aws.go:26" } { - "time": "2025-08-21 18:19:38", + "time": "2025-09-17 14:24:41", "level": "debug", "message": "uploading cloudformation template", "bucket": "splits-cf-templates", @@ -323,7 +400,7 @@ go test -tags=integration ./pkg/operator -v -race -run Test_Operator_Integration "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/infrastructure/aws.go:26" } { - "time": "2025-08-21 18:19:38", + "time": "2025-09-17 14:24:41", "level": "debug", "message": "uploading cloudformation template", "bucket": "splits-cf-templates", @@ -331,7 +408,7 @@ go test -tags=integration ./pkg/operator -v -race -run Test_Operator_Integration "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/infrastructure/aws.go:26" } { - "time": "2025-08-21 18:19:38", + "time": "2025-09-17 14:24:41", "level": "debug", "message": "uploading cloudformation template", "bucket": "splits-cf-templates", @@ -339,7 +416,7 @@ go test -tags=integration ./pkg/operator -v -race -run Test_Operator_Integration "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/infrastructure/aws.go:26" } { - "time": "2025-08-21 18:19:38", + "time": "2025-09-17 14:24:42", "level": "debug", "message": "uploading cloudformation template", "bucket": "splits-cf-templates", @@ -347,7 +424,7 @@ go test -tags=integration ./pkg/operator -v -race -run Test_Operator_Integration "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/infrastructure/aws.go:26" } { - "time": "2025-08-21 18:19:39", + "time": "2025-09-17 14:24:42", "level": "debug", "message": "uploading cloudformation template", "bucket": "splits-cf-templates", @@ -355,7 +432,7 @@ go test -tags=integration ./pkg/operator -v -race -run Test_Operator_Integration "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/infrastructure/aws.go:26" } { - "time": "2025-08-21 18:19:39", + "time": "2025-09-17 14:24:42", "level": "debug", "message": "uploading cloudformation template", "bucket": "splits-cf-templates", @@ -363,7 +440,7 @@ go test -tags=integration ./pkg/operator -v -race -run Test_Operator_Integration "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/infrastructure/aws.go:26" } { - "time": "2025-08-21 18:19:39", + "time": "2025-09-17 14:24:42", "level": "debug", "message": "uploading cloudformation template", "bucket": "splits-cf-templates", @@ -371,7 +448,7 @@ go test -tags=integration ./pkg/operator -v -race -run Test_Operator_Integration "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/infrastructure/aws.go:26" } { - "time": "2025-08-21 18:19:40", + "time": "2025-09-17 14:24:42", "level": "debug", "message": "uploading cloudformation template", "bucket": "splits-cf-templates", @@ -379,7 +456,7 @@ go test -tags=integration ./pkg/operator -v -race -run Test_Operator_Integration "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/infrastructure/aws.go:26" } { - "time": "2025-08-21 18:19:40", + "time": "2025-09-17 14:24:43", "level": "debug", "message": "uploading cloudformation template", "bucket": "splits-cf-templates", @@ -387,7 +464,15 @@ go test -tags=integration ./pkg/operator -v -race -run Test_Operator_Integration "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/infrastructure/aws.go:26" } { - "time": "2025-08-21 18:19:40", + "time": "2025-09-17 14:24:43", + "level": "debug", + "message": "uploading cloudformation template", + "bucket": "splits-cf-templates", + "key": "testing/splits-lite/splits-lite.yaml", + "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/infrastructure/aws.go:26" +} +{ + "time": "2025-09-17 14:24:43", "level": "debug", "message": "uploading cloudformation template", "bucket": "splits-cf-templates", @@ -395,7 +480,7 @@ go test -tags=integration ./pkg/operator -v -race -run Test_Operator_Integration "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/infrastructure/aws.go:26" } { - "time": "2025-08-21 18:19:40", + "time": "2025-09-17 14:24:44", "level": "debug", "message": "uploading cloudformation template", "bucket": "splits-cf-templates", @@ -403,33 +488,34 @@ go test -tags=integration ./pkg/operator -v -race -run Test_Operator_Integration "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/infrastructure/aws.go:26" } { - "time": "2025-08-21 18:19:40", + "time": "2025-09-17 14:24:44", "level": "debug", "message": "instrumented worker handler", "handler": "infrastructure", - "latency": "3.557394083s", + "latency": "3.811803709s", "success": "true", - "caller": "/Users/xh3b4sd/project/0xSplits/workit/handler/metrics/ensure.go:55" + "caller": "/Users/xh3b4sd/go/pkg/mod/github.com/0x!splits/workit@v0.6.0/handler/metrics/ensure.go:55" } { - "time": "2025-08-21 18:19:40", + "time": "2025-09-17 14:24:44", "level": "info", "message": "updating cloudformation stack", "name": "server-test", - "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/cloudformation/ensure.go:44" + "url": "https://splits-cf-templates.s3.us-west-2.amazonaws.com/testing/index.yaml", + "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/cloudformation/ensure.go:30" } { - "time": "2025-08-21 18:19:40", + "time": "2025-09-17 14:24:44", "level": "debug", "message": "instrumented worker handler", "handler": "cloudformation", - "latency": "190.917µs", + "latency": "419.375µs", "success": "true", - "caller": "/Users/xh3b4sd/project/0xSplits/workit/handler/metrics/ensure.go:55" + "caller": "/Users/xh3b4sd/go/pkg/mod/github.com/0x!splits/workit@v0.6.0/handler/metrics/ensure.go:55" } ---- PASS: Test_Operator_Integration (7.85s) +--- PASS: Test_Operator_Integration (10.67s) PASS -ok github.com/0xSplits/kayron/pkg/operator 9.139s +ok github.com/0xSplits/kayron/pkg/operator 11.969s ``` ### Releases diff --git a/pkg/cache/cache.go b/pkg/cache/cache.go index 10a339a..030c3db 100644 --- a/pkg/cache/cache.go +++ b/pkg/cache/cache.go @@ -31,7 +31,6 @@ type Cache struct { inf []Object log logger.Interface mut sync.Mutex - pre []Object ser []Object } @@ -45,7 +44,6 @@ func New(c Config) *Cache { inf: nil, log: c.Log, mut: sync.Mutex{}, - pre: nil, ser: nil, } } diff --git a/pkg/cache/create.go b/pkg/cache/create.go index c2f068f..856326b 100644 --- a/pkg/cache/create.go +++ b/pkg/cache/create.go @@ -45,15 +45,6 @@ func (c *Cache) Create(rel release.Slice) error { { c.inf = append(c.inf, obj) } - } else if bool(x.Deploy.Preview) { - { - obj.ind = len(c.pre) - obj.kin = Preview - } - - { - c.pre = append(c.pre, obj) - } } else { { obj.ind = len(c.ser) diff --git a/pkg/cache/delete.go b/pkg/cache/delete.go index b52cbf5..877159c 100644 --- a/pkg/cache/delete.go +++ b/pkg/cache/delete.go @@ -13,7 +13,6 @@ func (c *Cache) Delete() { { c.inf = nil - c.pre = nil c.ser = nil } } diff --git a/pkg/cache/object.go b/pkg/cache/object.go index 359754b..9b41520 100644 --- a/pkg/cache/object.go +++ b/pkg/cache/object.go @@ -16,7 +16,6 @@ type kind string const ( Infrastructure kind = "infrastructure" - Preview kind = "preview" Service kind = "service" ) @@ -36,7 +35,7 @@ func (o Object) Name() string { return o.Release.Github.String() } - if o.kin == Preview || o.kin == Service { + if o.kin == Service { return o.Release.Docker.String() } diff --git a/pkg/cache/previews.go b/pkg/cache/previews.go index 318ed4d..4258efb 100644 --- a/pkg/cache/previews.go +++ b/pkg/cache/previews.go @@ -1,5 +1,7 @@ package cache +// Previews returns all cached service release artifacts that are defined as +// preview deployments. func (c *Cache) Previews(doc string) []Object { { c.mut.Lock() @@ -8,8 +10,8 @@ func (c *Cache) Previews(doc string) []Object { var lis []Object - for _, x := range c.pre { - if x.Release.Docker.String() == doc { + for _, x := range c.ser { + if bool(x.Release.Deploy.Preview) && x.Release.Docker.String() == doc { lis = append(lis, x) } } diff --git a/pkg/cache/releases.go b/pkg/cache/releases.go index 0153463..af3ceb7 100644 --- a/pkg/cache/releases.go +++ b/pkg/cache/releases.go @@ -10,7 +10,6 @@ func (c *Cache) Releases() []Object { { lis = append(lis, c.inf...) - lis = append(lis, c.pre...) lis = append(lis, c.ser...) } diff --git a/pkg/cache/services.go b/pkg/cache/services.go index 8a1da84..3b7f04e 100644 --- a/pkg/cache/services.go +++ b/pkg/cache/services.go @@ -1,5 +1,7 @@ package cache +// Services returns all cached service release artifacts, including those of any +// preview deployments. func (c *Cache) Services() []Object { { c.mut.Lock() diff --git a/pkg/cache/update.go b/pkg/cache/update.go index 0c4741a..2e33c86 100644 --- a/pkg/cache/update.go +++ b/pkg/cache/update.go @@ -10,10 +10,6 @@ func (c *Cache) Update(obj Object) { c.inf[obj.ind].Artifact = c.inf[obj.ind].Artifact.Merge(obj.Artifact) } - if obj.kin == Preview { - c.pre[obj.ind].Artifact = c.pre[obj.ind].Artifact.Merge(obj.Artifact) - } - if obj.kin == Service { c.ser[obj.ind].Artifact = c.ser[obj.ind].Artifact.Merge(obj.Artifact) } diff --git a/pkg/hash/hash.go b/pkg/hash/hash.go index 14a0f49..4284d24 100644 --- a/pkg/hash/hash.go +++ b/pkg/hash/hash.go @@ -9,8 +9,8 @@ import ( ) type Hash struct { - Hsh []byte Dsh []byte + Hsh []byte } func New(str string) Hash { @@ -19,7 +19,15 @@ func New(str string) Hash { cas := cases.Upper(language.English).String(enc[:8]) return Hash{ - Hsh: []byte(cas), Dsh: []byte("-" + cas), + Hsh: []byte(cas), } } + +func (h Hash) Empty() bool { + return h.Dsh == nil && h.Hsh == nil +} + +func (h Hash) String() string { + return string(h.Hsh) +} diff --git a/pkg/operator/container/cache.go b/pkg/operator/container/cache.go index 717b30d..24a35a4 100644 --- a/pkg/operator/container/cache.go +++ b/pkg/operator/container/cache.go @@ -4,13 +4,14 @@ func (c *Container) cache(ima []image) { for _, x := range c.cac.Services() { var tag string { - tag = curTag(ima, x.Release.Docker.String()) + tag = curTag(ima, x.Release.Labels.Hash.String(), x.Release.Docker.String()) } c.log.Log( "level", "debug", "message", "caching current state", "docker", x.Release.Docker.String(), + "preview", x.Release.Deploy.Preview.String(), "current", musStr(tag), ) @@ -32,9 +33,9 @@ func (c *Container) cache(ima []image) { } } -func curTag(ima []image, ser string) string { +func curTag(ima []image, hsh string, doc string) string { for _, x := range ima { - if x.ser == ser { + if x.pre == hsh && x.ser == doc { return x.tag } } diff --git a/pkg/operator/container/image.go b/pkg/operator/container/image.go index cca8cc7..e7efa8f 100644 --- a/pkg/operator/container/image.go +++ b/pkg/operator/container/image.go @@ -11,6 +11,9 @@ import ( ) type image struct { + // pre is the "preview" resource tag attached to any given ECS service, if + // any, e.g. 1D0FD508. + pre string // ser is the "service" resource tag attached to any given ECS service, e.g. // alloy or specta. ser string @@ -57,6 +60,7 @@ func (c *Container) image(tas []task) ([]image, error) { // only responsible for their own execution index within the image slice. ima[i] = image{ + pre: t.pre, ser: t.ser, tag: tag, } diff --git a/pkg/operator/container/task.go b/pkg/operator/container/task.go index 4413b94..3ae23b9 100644 --- a/pkg/operator/container/task.go +++ b/pkg/operator/container/task.go @@ -15,6 +15,9 @@ type task struct { // arn is the filtered task definition ARN that any given ECS service is // running right now. arn string + // pre is the "preview" resource tag attached to any given ECS service, if + // any, e.g. 1D0FD508. + pre string // ser is the "service" resource tag attached to any given ECS service, e.g. // alloy or specta. ser string @@ -57,12 +60,14 @@ func (c *Container) task(det []detail) ([]task, error) { } for _, x := range out.Services { - var tag string + var pre string + var ser string { - tag = serTag(x.Tags) + pre = serTag(x.Tags, "preview") + ser = serTag(x.Tags, "service") } - if tag == "" { + if ser == "" { c.log.Log( "level", "warning", "message", "skipping reconciliation for ECS service", @@ -81,7 +86,8 @@ func (c *Container) task(det []detail) ([]task, error) { tas[i] = task{ arn: *x.TaskDefinition, - ser: tag, + pre: pre, + ser: ser, } } @@ -121,9 +127,9 @@ func (c *Container) task(det []detail) ([]task, error) { return fil, nil } -func serTag(tag []types.Tag) string { +func serTag(tag []types.Tag, key string) string { for _, x := range tag { - if *x.Key == "service" { + if x.Key != nil && x.Value != nil && *x.Key == key { return *x.Value } } diff --git a/pkg/operator/infrastructure/ensure.go b/pkg/operator/infrastructure/ensure.go index 3aed1ef..38313bc 100644 --- a/pkg/operator/infrastructure/ensure.go +++ b/pkg/operator/infrastructure/ensure.go @@ -64,6 +64,11 @@ func (i *Infrastructure) Ensure() error { } } + var nam string + { + nam = strings.TrimSuffix(fil.Name(), ext) + } + var byt []byte { byt, err = afero.ReadFile(gfs, pat) @@ -76,24 +81,11 @@ func (i *Infrastructure) Ensure() error { // deployments configured for the service release that matches this // particular template by file name. // - // "lite.yaml" == Release.Docker.String() + // "splits-lite" == Release.Docker.String() // - var pre *preview.Preview - { - pre = preview.New(preview.Config{ - Env: i.env, - Inp: byt, - }) - } - - var rep string - { - rep = strings.TrimSuffix(fil.Name(), ext) - } - { - byt, err = pre.Render(i.cac.Previews(rep)) + byt, err = i.renPre(nam, byt) if err != nil { return tracer.Mask(err) } @@ -118,3 +110,41 @@ func (i *Infrastructure) Ensure() error { return nil } + +func (i *Infrastructure) renPre(nam string, byt []byte) ([]byte, error) { + var err error + + // If there are no preview deployments defined inside our release artifacts, + // then we do not have to inject anything, but instead return the same + // template bytes early that we just received as input. + + var rel []cache.Object + { + rel = i.cac.Previews(nam) + } + + if len(rel) == 0 { + return byt, nil + } + + // At this point we have preview deployments defined by at least one release + // artifact. So we create a preview renderer and extend the raw template bytes + // that we received as input data above. + + var pre *preview.Preview + { + pre = preview.New(preview.Config{ + Env: i.env, + Inp: byt, + }) + } + + { + byt, err = pre.Render(rel) + if err != nil { + return nil, tracer.Mask(err) + } + } + + return byt, nil +} diff --git a/pkg/operator/policy/ensure.go b/pkg/operator/policy/ensure.go index a2cf2b7..a09980e 100644 --- a/pkg/operator/policy/ensure.go +++ b/pkg/operator/policy/ensure.go @@ -64,6 +64,7 @@ func (p *Policy) ensure(rel []cache.Object) error { "message", "continuing reconciliation loop", "reason", "detected state drift", "release", x.Name(), + "preview", x.Release.Deploy.Preview.String(), "version", x.Artifact.Reference.Desired, ) diff --git a/pkg/operator/reference/ensure.go b/pkg/operator/reference/ensure.go index aac2964..2519471 100644 --- a/pkg/operator/reference/ensure.go +++ b/pkg/operator/reference/ensure.go @@ -18,12 +18,24 @@ func (r *Reference) Ensure() error { // Find the reference for every branch deployment strategy. The concurrently // executed function below prevents network calls for every release that does - // not define a branch deployment strategy. + // not define a branch deployment strategy. Note that we also do not lookup + // branch references for preview deployments, if those references do already + // exist. E.g. we may have looked up the latest commit sha for a preview + // deployment in an earlier stage of this reconciliation loop. fnc := func(i int, x cache.Object) error { - ref, err := r.desRef(x.Release) - if err != nil { - return tracer.Mask(err) + var err error + + if bool(x.Release.Deploy.Preview) && x.Artifact.Reference.Desired != "" { + return nil + } + + var ref string + { + ref, err = r.desRef(x.Release) + if err != nil { + return tracer.Mask(err) + } } if ref == "" { @@ -34,6 +46,7 @@ func (r *Reference) Ensure() error { "level", "debug", "message", "caching desired state", "github", x.Release.Github.String(), + "preview", x.Release.Deploy.Preview.String(), "desired", musStr(ref), ) diff --git a/pkg/operator/registry/ensure.go b/pkg/operator/registry/ensure.go index 4fcce1b..5c12f0a 100644 --- a/pkg/operator/registry/ensure.go +++ b/pkg/operator/registry/ensure.go @@ -42,22 +42,23 @@ func (r *Registry) Ensure() error { } } - { - x.Artifact.Condition.Success = exi - } - - { - r.cac.Update(x) - } - r.log.Log( "level", "debug", "message", "executed image check", "image", x.Release.Docker.String(), + "preview", x.Release.Deploy.Preview.String(), "tag", des, "exists", strconv.FormatBool(exi), ) + { + x.Artifact.Condition.Success = exi + } + + { + r.cac.Update(x) + } + return nil } diff --git a/pkg/operator/release/ensure.go b/pkg/operator/release/ensure.go index ae42e89..076d834 100644 --- a/pkg/operator/release/ensure.go +++ b/pkg/operator/release/ensure.go @@ -148,5 +148,11 @@ func (r *Release) Ensure() error { } } + // TODO there is a potential efficiency benefit to be had if we expanded cache + // objects instead of release definitions, because we are already fetching the + // latest Git SHAs for every preview deployment in Preview.Expand above. With + // that we should probably also move all of this cache object expansion for + // preview deployments into its own operator handler/function. + return nil } diff --git a/pkg/preview/domain.go b/pkg/preview/domain.go new file mode 100644 index 0000000..5e1b57c --- /dev/null +++ b/pkg/preview/domain.go @@ -0,0 +1,22 @@ +package preview + +import ( + "fmt" + "strings" +) + +func preDom(hsh string, doc string) string { + // Note that this is a dirty hack to make preview deployments work today for + // existing services that already work using certain incosnistencies between + // repository and domain names. E.g. we have "splits-lite" in Github, but use + // just "lite.testing.splits.org". A better way of doing this would be to allow + // for some kind of domain configuration in the release definition, so that we + // can remove this magical string replacement below. + + var trm string + { + trm = strings.TrimPrefix(doc, "splits-") + } + + return fmt.Sprintf("%s.%s.${Environment}.splits.org", hsh, trm) +} diff --git a/pkg/preview/expand.go b/pkg/preview/expand.go index f906685..7fcf4a1 100644 --- a/pkg/preview/expand.go +++ b/pkg/preview/expand.go @@ -3,7 +3,9 @@ package preview import ( "context" "sort" + "strings" + "github.com/0xSplits/kayron/pkg/hash" "github.com/0xSplits/kayron/pkg/release/schema/release" "github.com/0xSplits/kayron/pkg/release/schema/release/deploy" "github.com/0xSplits/kayron/pkg/release/schema/release/deploy/branch" @@ -12,6 +14,15 @@ import ( "github.com/xh3b4sd/tracer" ) +var ( + // filter is a collection of branch name prefixes that we want to ignore when + // expanding a service release into preview releases. E.g. we do not want to + // deploy preview releases for dependabot branches. + filter = []string{ + "dependabot/", + } +) + func (p *Preview) Expand(rel release.Struct) (release.Slice, error) { var err error @@ -39,13 +50,25 @@ func (p *Preview) Expand(rel release.Struct) (release.Slice, error) { } func expand(rel release.Struct, pul []*github.PullRequest) release.Slice { - // Sort pull requests from oldest to newest, so that new pull requests do not - // change the order of pull request specific preview deployments. This is - // relevant because the order of preview releases created below will define - // the priority settings of the ALB's listener rules. + // Before sorting, filter pull requests by branch names that we definitely + // want to consider for preview releases. E.g. drop all dependabot branches + // before sorting. - sort.Slice(pul, func(i, j int) bool { - return pul[i].GetCreatedAt().Before(pul[j].GetCreatedAt().Time) + var fil []*github.PullRequest + + for _, x := range pul { + if !hasPre(x.GetHead().GetRef(), filter) { + fil = append(fil, x) + } + } + + // Sort our filtered pull requests from oldest to newest, so that new pull + // requests do not change the order of pull request specific preview + // deployments. This is relevant because the order of preview releases created + // below will define the priority settings of the ALB's listener rules. + + sort.Slice(fil, func(i, j int) bool { + return fil[i].GetCreatedAt().Before(fil[j].GetCreatedAt().Time) }) // Mark the expanded service release as non-preview. The Deploy.Preview @@ -64,19 +87,32 @@ func expand(rel release.Struct, pul []*github.PullRequest) release.Slice { lis = append(lis, rel) } - for _, x := range pul { + for _, x := range fil { var pre release.Struct { pre = rel } + var bra string + { + bra = x.GetHead().GetRef() + } + { pre.Deploy = deploy.Struct{ - Branch: branch.String(x.GetHead().GetRef()), + Branch: branch.String(bra), Preview: preview.Bool(true), } } + // Make sure to inject the preview deployment hash into the release labels. + // This is used to identify the correct current state of deployed container + // image tags, as well as rendering the correct CloudFormation templates. + + { + pre.Labels.Hash = hash.New(bra) + } + { lis = append(lis, pre) } @@ -84,3 +120,13 @@ func expand(rel release.Struct, pul []*github.PullRequest) release.Slice { return lis } + +func hasPre(str string, pre []string) bool { + for _, x := range pre { + if strings.HasPrefix(str, x) { + return true + } + } + + return false +} diff --git a/pkg/preview/expand_test.go b/pkg/preview/expand_test.go index 087214f..a36023f 100644 --- a/pkg/preview/expand_test.go +++ b/pkg/preview/expand_test.go @@ -5,11 +5,13 @@ import ( "testing" "time" + "github.com/0xSplits/kayron/pkg/hash" "github.com/0xSplits/kayron/pkg/release/schema/release" "github.com/0xSplits/kayron/pkg/release/schema/release/deploy" "github.com/0xSplits/kayron/pkg/release/schema/release/deploy/branch" "github.com/0xSplits/kayron/pkg/release/schema/release/deploy/preview" "github.com/0xSplits/kayron/pkg/release/schema/release/docker" + "github.com/0xSplits/kayron/pkg/release/schema/release/labels" "github.com/google/go-cmp/cmp" "github.com/google/go-github/v73/github" ) @@ -31,6 +33,7 @@ func Test_Preview_Expand(t *testing.T) { }, pul: []*github.PullRequest{ {CreatedAt: tesTim(3), Head: tesBra("b/3")}, + {CreatedAt: tesTim(4), Head: tesBra("dependabot/foo-bar")}, {CreatedAt: tesTim(5), Head: tesBra("b/5")}, }, exp: release.Slice{ @@ -47,6 +50,9 @@ func Test_Preview_Expand(t *testing.T) { Preview: preview.Bool(true), }, Docker: docker.String("lite"), + Labels: labels.Struct{ + Hash: hash.New("b/3"), + }, }, { Deploy: deploy.Struct{ @@ -54,6 +60,9 @@ func Test_Preview_Expand(t *testing.T) { Preview: preview.Bool(true), }, Docker: docker.String("lite"), + Labels: labels.Struct{ + Hash: hash.New("b/5"), + }, }, }, }, @@ -68,8 +77,11 @@ func Test_Preview_Expand(t *testing.T) { }, pul: []*github.PullRequest{ {CreatedAt: tesTim(3), Head: tesBra("b/3")}, + {CreatedAt: tesTim(4), Head: tesBra("dependabot/foo-bar")}, {CreatedAt: tesTim(5), Head: tesBra("b/5")}, {CreatedAt: tesTim(7), Head: tesBra("b/7")}, + {CreatedAt: tesTim(4), Head: tesBra("dependabot/another-one")}, + {CreatedAt: tesTim(4), Head: tesBra("dependabot/b/5")}, {CreatedAt: tesTim(9), Head: tesBra("b/9")}, }, exp: release.Slice{ @@ -86,6 +98,9 @@ func Test_Preview_Expand(t *testing.T) { Preview: preview.Bool(true), }, Docker: docker.String("lite"), + Labels: labels.Struct{ + Hash: hash.New("b/3"), + }, }, { Deploy: deploy.Struct{ @@ -93,6 +108,9 @@ func Test_Preview_Expand(t *testing.T) { Preview: preview.Bool(true), }, Docker: docker.String("lite"), + Labels: labels.Struct{ + Hash: hash.New("b/5"), + }, }, { Deploy: deploy.Struct{ @@ -100,6 +118,9 @@ func Test_Preview_Expand(t *testing.T) { Preview: preview.Bool(true), }, Docker: docker.String("lite"), + Labels: labels.Struct{ + Hash: hash.New("b/7"), + }, }, { Deploy: deploy.Struct{ @@ -107,6 +128,9 @@ func Test_Preview_Expand(t *testing.T) { Preview: preview.Bool(true), }, Docker: docker.String("lite"), + Labels: labels.Struct{ + Hash: hash.New("b/9"), + }, }, }, }, diff --git a/pkg/preview/render.go b/pkg/preview/render.go index 6226369..2eb6f68 100644 --- a/pkg/preview/render.go +++ b/pkg/preview/render.go @@ -9,25 +9,9 @@ import ( "github.com/xh3b4sd/tracer" ) -var ( - header = bytes.Join( - [][]byte{ - []byte(" #"), - []byte(" # AUTO GENERATED PREVIEW DEPLOYMENT"), - []byte(" #"), - }, - []byte("\n"), - ) -) - func (p *Preview) Render(pre []cache.Object) ([]byte, error) { var err error - // TODO write test - if len(pre) == 0 { - return p.inp, nil - } - var res Resource { res = Resource{ @@ -58,12 +42,12 @@ func (p *Preview) Render(pre []cache.Object) ([]byte, error) { for _, x := range pre { var hsh hash.Hash { - hsh = hash.New(x.Release.Deploy.Branch.String()) + hsh = x.Release.Labels.Hash } var dom string { - dom = fmt.Sprintf("%s.%s.${Environment}.splits.org", hsh.Hsh, x.Release.Docker.String()) + dom = preDom(hsh.String(), x.Release.Docker.String()) } var ima []byte @@ -74,6 +58,14 @@ func (p *Preview) Render(pre []cache.Object) ([]byte, error) { } } + var tag []byte + { + tag, err = appTag(res.Ser.Search([]byte(" Tags:")).Bytes(), hsh.String()) + if err != nil { + return nil, tracer.Mask(err) + } + } + // Increment the listener rule priority per preview release. This must be // done before the call to render(), because the base priority resolved // above is already taken by the main release defining our preview @@ -84,20 +76,21 @@ func (p *Preview) Render(pre []cache.Object) ([]byte, error) { } { - out = append(out, p.render(res, dom, pri, hsh, ima)...) + out = append(out, p.render(res, dom, pri, hsh, ima, tag)...) } } return out, nil } -func (p *Preview) render(res Resource, dom string, pri int, hsh hash.Hash, ima []byte) []byte { +func (p *Preview) render(res Resource, dom string, pri int, hsh hash.Hash, ima []byte, tag []byte) []byte { { res.Ser = res.Ser.Append([]byte(" Service:"), hsh.Hsh) res.Ser = res.Ser.Append([]byte(" ServiceName:"), hsh.Dsh) res.Ser = res.Ser.Append([]byte(" TaskDefinition:"), hsh.Hsh) res.Ser = res.Ser.Append([]byte(" - TargetGroupArn:"), hsh.Hsh) res.Ser = res.Ser.Delete([]byte(" ServiceRegistries:")) + res.Ser = res.Ser.Delete([]byte(" Tags:"), tag...) } { @@ -124,7 +117,7 @@ func (p *Preview) render(res Resource, dom string, pri int, hsh hash.Hash, ima [ var out []byte { - out = append(out, header...) + out = append(out, header(hsh)...) out = append(out, '\n', '\n') out = append(out, res.Ser.Bytes()...) out = append(out, '\n', '\n') @@ -140,3 +133,14 @@ func (p *Preview) render(res Resource, dom string, pri int, hsh hash.Hash, ima [ return out } + +func header(hsh hash.Hash) []byte { + return bytes.Join( + [][]byte{ + []byte(" #"), + []byte(" # AUTO GENERATED PREVIEW DEPLOYMENT " + hsh.String()), + []byte(" #"), + }, + []byte("\n"), + ) +} diff --git a/pkg/preview/render_test.go b/pkg/preview/render_test.go index 492f4cd..8ce80ca 100644 --- a/pkg/preview/render_test.go +++ b/pkg/preview/render_test.go @@ -8,16 +8,18 @@ import ( "github.com/0xSplits/kayron/pkg/cache" "github.com/0xSplits/kayron/pkg/envvar" + "github.com/0xSplits/kayron/pkg/hash" "github.com/0xSplits/kayron/pkg/release/artifact" "github.com/0xSplits/kayron/pkg/release/artifact/reference" "github.com/0xSplits/kayron/pkg/release/schema/release" "github.com/0xSplits/kayron/pkg/release/schema/release/deploy" "github.com/0xSplits/kayron/pkg/release/schema/release/deploy/branch" "github.com/0xSplits/kayron/pkg/release/schema/release/docker" + "github.com/0xSplits/kayron/pkg/release/schema/release/labels" "github.com/google/go-cmp/cmp" ) -func Test_Operator_Infrastructure_Preview_Render(t *testing.T) { +func Test_Preview_Render(t *testing.T) { testCases := []struct { obj []cache.Object }{ @@ -35,6 +37,9 @@ func Test_Operator_Infrastructure_Preview_Render(t *testing.T) { Branch: branch.String("fancy-feature-branch"), }, Docker: docker.String("lite"), + Labels: labels.Struct{ + Hash: hash.New("fancy-feature-branch"), + }, }, }, { @@ -48,6 +53,9 @@ func Test_Operator_Infrastructure_Preview_Render(t *testing.T) { Branch: branch.String("dependabot/another-one"), }, Docker: docker.String("lite"), + Labels: labels.Struct{ + Hash: hash.New("dependabot/another-one"), + }, }, }, }, diff --git a/pkg/preview/tags.go b/pkg/preview/tags.go new file mode 100644 index 0000000..14c28f2 --- /dev/null +++ b/pkg/preview/tags.go @@ -0,0 +1,26 @@ +package preview + +import ( + "bytes" +) + +func appTag(lin []byte, hsh string) ([]byte, error) { + var pre []byte + { + pre = bytes.Join( + [][]byte{ + nil, + []byte(" - Key: \"preview\""), + []byte(" Value: \"" + hsh + "\""), + }, + []byte("\n"), + ) + } + + var byt []byte + { + byt = append(lin, pre...) + } + + return byt, nil +} diff --git a/pkg/preview/testdata/000/out.yaml.golden b/pkg/preview/testdata/000/out.yaml.golden index cf3413c..f151eea 100644 --- a/pkg/preview/testdata/000/out.yaml.golden +++ b/pkg/preview/testdata/000/out.yaml.golden @@ -308,7 +308,7 @@ Resources: TTL: 60 # - # AUTO GENERATED PREVIEW DEPLOYMENT + # AUTO GENERATED PREVIEW DEPLOYMENT 1D0FD508 # Service1D0FD508: @@ -336,6 +336,8 @@ Resources: Value: !Ref Environment - Key: "service" Value: "splits-lite" + - Key: "preview" + Value: "1D0FD508" DependsOn: - Cluster @@ -417,7 +419,7 @@ Resources: Priority: 12001 # Host header = 1D0FD508.lite.${Environment}.splits.org # - # AUTO GENERATED PREVIEW DEPLOYMENT + # AUTO GENERATED PREVIEW DEPLOYMENT F4436797 # ServiceF4436797: @@ -445,6 +447,8 @@ Resources: Value: !Ref Environment - Key: "service" Value: "splits-lite" + - Key: "preview" + Value: "F4436797" DependsOn: - Cluster diff --git a/pkg/release/schema/release/deploy/struct.go b/pkg/release/schema/release/deploy/struct.go index b337943..4d0a502 100644 --- a/pkg/release/schema/release/deploy/struct.go +++ b/pkg/release/schema/release/deploy/struct.go @@ -1,6 +1,8 @@ package deploy import ( + "fmt" + "github.com/0xSplits/kayron/pkg/release/schema/release/deploy/branch" "github.com/0xSplits/kayron/pkg/release/schema/release/deploy/preview" "github.com/0xSplits/kayron/pkg/release/schema/release/deploy/release" @@ -28,7 +30,7 @@ func (s Struct) Empty() bool { func (s Struct) String() string { if !s.Branch.Empty() { - return s.Branch.String() + return fmt.Sprintf("branch=%s", s.Branch.String()) } // Note that Struct.Preview is not a deployment strategy in all environments, @@ -36,15 +38,15 @@ func (s Struct) String() string { // this deployment strategy. if !s.Release.Empty() { - return s.Release.String() + return fmt.Sprintf("release=%s", s.Release.String()) } if !s.Suspend.Empty() { - return s.Suspend.String() + return fmt.Sprintf("suspend=%s", s.Suspend.String()) } if !s.Webhook.Empty() { - return s.Webhook.String() + return fmt.Sprintf("webhook=%s", s.Webhook.String()) } return "" diff --git a/pkg/release/schema/release/labels/struct.go b/pkg/release/schema/release/labels/struct.go index 08131ae..7a1b691 100644 --- a/pkg/release/schema/release/labels/struct.go +++ b/pkg/release/schema/release/labels/struct.go @@ -1,5 +1,9 @@ package labels +import ( + "github.com/0xSplits/kayron/pkg/hash" +) + // Struct contains runtime specific internals annotated inside the schema // loader. type Struct struct { @@ -7,16 +11,19 @@ type Struct struct { // of a specific config file. E.g. given a config file with 3 service // definitions, the last block has the index 2. Block int + // Hash contains the hashed branch name for any service release of a preview + // deployment. + Hash hash.Hash // Source is the absolute source file path of the .yaml definition as loaded // from the underlying file system. This label may help to make error messages // more useful. Source string } -func (m Struct) Empty() bool { - return m.Source == "" +func (s Struct) Empty() bool { + return s.Block == 0 && s.Hash.Empty() && s.Source == "" } -func (m Struct) Verify() error { +func (s Struct) Verify() error { return nil } diff --git a/pkg/scanner/search.go b/pkg/scanner/search.go index 3d93c56..7451dc3 100644 --- a/pkg/scanner/search.go +++ b/pkg/scanner/search.go @@ -29,7 +29,7 @@ func (s *Scanner) Search(key []byte) *Scanner { end = spaces(lin) } - if fou && sta == end && len(lin) != 0 { + if fou && end <= sta && len(lin) != 0 { break } diff --git a/pkg/scanner/search_test.go b/pkg/scanner/search_test.go index 6bbd90f..f250ed3 100644 --- a/pkg/scanner/search_test.go +++ b/pkg/scanner/search_test.go @@ -33,6 +33,10 @@ func Test_Scanner_Search(t *testing.T) { { key: " Image:", }, + // Case 005 + { + key: " Tags:", + }, } for i, tc := range testCases { diff --git a/pkg/scanner/testdata/search/005/inp.yaml.golden b/pkg/scanner/testdata/search/005/inp.yaml.golden new file mode 100644 index 0000000..bf01872 --- /dev/null +++ b/pkg/scanner/testdata/search/005/inp.yaml.golden @@ -0,0 +1,32 @@ + Service: + Type: AWS::ECS::Service + Properties: + Cluster: !Ref Cluster + ServiceName: !Sub "${AWS::StackName}-lite" + DesiredCount: 1 + LaunchType: "FARGATE" + TaskDefinition: !Ref TaskDefinition + NetworkConfiguration: + AwsvpcConfiguration: + AssignPublicIp: "DISABLED" + SecurityGroups: + - !Ref SecurityGroup + Subnets: + - Fn::ImportValue: !Sub "${NetworkStack}-PrivateSubnet1ID" + - Fn::ImportValue: !Sub "${NetworkStack}-PrivateSubnet2ID" + LoadBalancers: + - TargetGroupArn: !Ref TargetGroup + ContainerName: !Sub "${AWS::StackName}-lite" + ContainerPort: !Ref LitePort + ServiceRegistries: + - ContainerName: !Sub "${AWS::StackName}-lite" + RegistryArn: !GetAtt ServiceDiscovery.Arn + Tags: + - Key: "environment" + Value: !Ref Environment + - Key: "service" + Value: "splits-lite" # must match repo name + DependsOn: + - Cluster + - TaskDefinition + - ListenerRule diff --git a/pkg/scanner/testdata/search/005/out.yaml.golden b/pkg/scanner/testdata/search/005/out.yaml.golden new file mode 100644 index 0000000..d84f802 --- /dev/null +++ b/pkg/scanner/testdata/search/005/out.yaml.golden @@ -0,0 +1,5 @@ + Tags: + - Key: "environment" + Value: !Ref Environment + - Key: "service" + Value: "splits-lite" # must match repo name From 6f795f5294d2daa4b206fb60d8debba4aef2d71f Mon Sep 17 00:00:00 2001 From: xh3b4sd Date: Wed, 17 Sep 2025 17:50:15 +0200 Subject: [PATCH 07/13] fix --- README.md | 169 ++++++++++++---------- pkg/cache/object.go | 28 ++++ pkg/operator/cloudformation/ensure.go | 12 +- pkg/operator/container/task.go | 21 ++- pkg/operator/operator.go | 2 +- pkg/operator/operator_integration_test.go | 1 + pkg/operator/policy/ensure.go | 14 +- pkg/operator/policy/policy.go | 7 + pkg/preview/domain.go | 22 --- pkg/preview/render.go | 2 +- pkg/stack/search.go | 2 +- 11 files changed, 172 insertions(+), 108 deletions(-) delete mode 100644 pkg/preview/domain.go diff --git a/README.md b/README.md index e9a4e73..8a8204c 100644 --- a/README.md +++ b/README.md @@ -147,13 +147,13 @@ go test -tags=integration ./pkg/operator -v -race -run Test_Operator_Integration ```yaml === RUN Test_Operator_Integration { - "time": "2025-09-17 14:24:33", + "time": "2025-09-17 15:31:59", "level": "debug", "message": "resetting operator cache", "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/cache/delete.go:9" } { - "time": "2025-09-17 14:24:35", + "time": "2025-09-17 15:32:01", "level": "debug", "message": "resolved ref for github repository", "environment": "testing", @@ -162,7 +162,7 @@ go test -tags=integration ./pkg/operator -v -race -run Test_Operator_Integration "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/release/ensure.go:66" } { - "time": "2025-09-17 14:24:37", + "time": "2025-09-17 15:32:02", "level": "debug", "message": "caching release artifact", "deploy": "branch=preview", @@ -172,7 +172,7 @@ go test -tags=integration ./pkg/operator -v -race -run Test_Operator_Integration "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/cache/create.go:17" } { - "time": "2025-09-17 14:24:37", + "time": "2025-09-17 15:32:02", "level": "debug", "message": "caching release artifact", "deploy": "release=v0.1.1", @@ -182,7 +182,7 @@ go test -tags=integration ./pkg/operator -v -race -run Test_Operator_Integration "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/cache/create.go:17" } { - "time": "2025-09-17 14:24:37", + "time": "2025-09-17 15:32:02", "level": "debug", "message": "caching release artifact", "deploy": "branch=fancy-feature-branch", @@ -192,7 +192,7 @@ go test -tags=integration ./pkg/operator -v -race -run Test_Operator_Integration "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/cache/create.go:17" } { - "time": "2025-09-17 14:24:37", + "time": "2025-09-17 15:32:02", "level": "debug", "message": "caching release artifact", "deploy": "branch=preview", @@ -202,7 +202,7 @@ go test -tags=integration ./pkg/operator -v -race -run Test_Operator_Integration "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/cache/create.go:17" } { - "time": "2025-09-17 14:24:37", + "time": "2025-09-17 15:32:02", "level": "debug", "message": "caching release artifact", "deploy": "release=v0.2.2", @@ -212,33 +212,33 @@ go test -tags=integration ./pkg/operator -v -race -run Test_Operator_Integration "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/cache/create.go:17" } { - "time": "2025-09-17 14:24:37", + "time": "2025-09-17 15:32:02", "level": "debug", "message": "instrumented worker handler", "handler": "release", - "latency": "3.93228675s", + "latency": "3.727451s", "success": "true", "caller": "/Users/xh3b4sd/go/pkg/mod/github.com/0x!splits/workit@v0.6.0/handler/metrics/ensure.go:55" } { - "time": "2025-09-17 14:24:37", - "level": "debug", - "message": "caching desired state", - "desired": "v0.2.2", - "github": "specta", - "preview": "false", - "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/reference/ensure.go:45" -} -{ - "time": "2025-09-17 14:24:37", + "time": "2025-09-17 15:32:02", "level": "debug", "message": "caching current state", - "current": "3c113413a19f82f906f45ba22c2cd17bb7a62682", + "current": "d5fa88afd502edc9052f89c956618b2cb567d984", "github": "infrastructure", "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/template/ensure.go:35" } { - "time": "2025-09-17 14:24:37", + "time": "2025-09-17 15:32:02", + "level": "debug", + "message": "instrumented worker handler", + "handler": "template", + "latency": "173.083µs", + "success": "true", + "caller": "/Users/xh3b4sd/go/pkg/mod/github.com/0x!splits/workit@v0.6.0/handler/metrics/ensure.go:55" +} +{ + "time": "2025-09-17 15:32:02", "level": "debug", "message": "caching desired state", "desired": "v0.1.1", @@ -247,16 +247,16 @@ go test -tags=integration ./pkg/operator -v -race -run Test_Operator_Integration "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/reference/ensure.go:45" } { - "time": "2025-09-17 14:24:37", + "time": "2025-09-17 15:32:02", "level": "debug", - "message": "instrumented worker handler", - "handler": "template", - "latency": "238.584µs", - "success": "true", - "caller": "/Users/xh3b4sd/go/pkg/mod/github.com/0x!splits/workit@v0.6.0/handler/metrics/ensure.go:55" + "message": "caching desired state", + "desired": "v0.2.2", + "github": "specta", + "preview": "false", + "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/reference/ensure.go:45" } { - "time": "2025-09-17 14:24:37", + "time": "2025-09-17 15:32:03", "level": "debug", "message": "caching desired state", "desired": "d5fa88afd502edc9052f89c956618b2cb567d984", @@ -265,61 +265,61 @@ go test -tags=integration ./pkg/operator -v -race -run Test_Operator_Integration "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/reference/ensure.go:45" } { - "time": "2025-09-17 14:24:37", + "time": "2025-09-17 15:32:03", "level": "debug", "message": "caching desired state", - "desired": "1814c2027b7aef51e38e110c45ae5b4b79b6f856", + "desired": "eb9f56e195f25218499897a6c6d79c62261faba5", "github": "kayron", "preview": "false", "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/reference/ensure.go:45" } { - "time": "2025-09-17 14:24:37", + "time": "2025-09-17 15:32:03", "level": "debug", "message": "caching desired state", - "desired": "bc7891268e44f62e0aebbe339c0850b61d52c417", + "desired": "e307253e62c2da119579e2505db0b6185642ab9b", "github": "splits-lite", "preview": "true", "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/reference/ensure.go:45" } { - "time": "2025-09-17 14:24:37", + "time": "2025-09-17 15:32:03", "level": "debug", "message": "instrumented worker handler", "handler": "reference", - "latency": "304.052167ms", + "latency": "367.2485ms", "success": "true", "caller": "/Users/xh3b4sd/go/pkg/mod/github.com/0x!splits/workit@v0.6.0/handler/metrics/ensure.go:55" } { - "time": "2025-09-17 14:24:39", + "time": "2025-09-17 15:32:04", "level": "debug", "message": "caching current state", - "current": "v0.1.1", + "current": "bc7891268e44f62e0aebbe339c0850b61d52c417", "docker": "splits-lite", "preview": "false", "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/container/cache.go:10" } { - "time": "2025-09-17 14:24:39", + "time": "2025-09-17 15:32:04", "level": "debug", "message": "caching current state", - "current": "''", + "current": "bc7891268e44f62e0aebbe339c0850b61d52c417", "docker": "splits-lite", "preview": "true", "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/container/cache.go:10" } { - "time": "2025-09-17 14:24:39", + "time": "2025-09-17 15:32:04", "level": "debug", "message": "caching current state", - "current": "1814c2027b7aef51e38e110c45ae5b4b79b6f856", + "current": "eb9f56e195f25218499897a6c6d79c62261faba5", "docker": "kayron", "preview": "false", "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/container/cache.go:10" } { - "time": "2025-09-17 14:24:39", + "time": "2025-09-17 15:32:04", "level": "debug", "message": "caching current state", "current": "v0.2.2", @@ -328,54 +328,73 @@ go test -tags=integration ./pkg/operator -v -race -run Test_Operator_Integration "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/container/cache.go:10" } { - "time": "2025-09-17 14:24:39", + "time": "2025-09-17 15:32:04", "level": "debug", "message": "instrumented worker handler", "handler": "container", - "latency": "1.884508333s", + "latency": "1.705606292s", "success": "true", "caller": "/Users/xh3b4sd/go/pkg/mod/github.com/0x!splits/workit@v0.6.0/handler/metrics/ensure.go:55" } { - "time": "2025-09-17 14:24:40", + "time": "2025-09-17 15:32:05", "level": "debug", "message": "executed image check", "exists": "true", "image": "splits-lite", "preview": "true", - "tag": "bc7891268e44f62e0aebbe339c0850b61d52c417", + "tag": "e307253e62c2da119579e2505db0b6185642ab9b", "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/registry/ensure.go:45" } { - "time": "2025-09-17 14:24:40", + "time": "2025-09-17 15:32:05", + "level": "debug", + "message": "executed image check", + "exists": "true", + "image": "splits-lite", + "preview": "false", + "tag": "v0.1.1", + "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/registry/ensure.go:45" +} +{ + "time": "2025-09-17 15:32:05", "level": "debug", "message": "instrumented worker handler", "handler": "registry", - "latency": "1.037679042s", + "latency": "875.278125ms", "success": "true", "caller": "/Users/xh3b4sd/go/pkg/mod/github.com/0x!splits/workit@v0.6.0/handler/metrics/ensure.go:55" } { - "time": "2025-09-17 14:24:40", + "time": "2025-09-17 15:32:05", "level": "info", "message": "continuing reconciliation loop", - "preview": "false", "reason": "detected state drift", - "release": "infrastructure", - "version": "d5fa88afd502edc9052f89c956618b2cb567d984", - "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/policy/ensure.go:62" + "release": "splits-lite", + "version": "v0.1.1", + "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/policy/ensure.go:68" +} +{ + "time": "2025-09-17 15:32:05", + "level": "info", + "message": "continuing reconciliation loop", + "domain": "1D0FD508.lite.testing.splits.org", + "reason": "detected state drift", + "release": "splits-lite", + "version": "e307253e62c2da119579e2505db0b6185642ab9b", + "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/policy/ensure.go:68" } { - "time": "2025-09-17 14:24:40", + "time": "2025-09-17 15:32:05", "level": "debug", "message": "instrumented worker handler", "handler": "policy", - "latency": "277.75µs", + "latency": "523.166µs", "success": "true", "caller": "/Users/xh3b4sd/go/pkg/mod/github.com/0x!splits/workit@v0.6.0/handler/metrics/ensure.go:55" } { - "time": "2025-09-17 14:24:40", + "time": "2025-09-17 15:32:05", "level": "debug", "message": "resolved ref for github repository", "environment": "testing", @@ -384,7 +403,7 @@ go test -tags=integration ./pkg/operator -v -race -run Test_Operator_Integration "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/infrastructure/ensure.go:24" } { - "time": "2025-09-17 14:24:40", + "time": "2025-09-17 15:32:05", "level": "debug", "message": "uploading cloudformation template", "bucket": "splits-cf-templates", @@ -392,7 +411,7 @@ go test -tags=integration ./pkg/operator -v -race -run Test_Operator_Integration "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/infrastructure/aws.go:26" } { - "time": "2025-09-17 14:24:41", + "time": "2025-09-17 15:32:06", "level": "debug", "message": "uploading cloudformation template", "bucket": "splits-cf-templates", @@ -400,7 +419,7 @@ go test -tags=integration ./pkg/operator -v -race -run Test_Operator_Integration "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/infrastructure/aws.go:26" } { - "time": "2025-09-17 14:24:41", + "time": "2025-09-17 15:32:06", "level": "debug", "message": "uploading cloudformation template", "bucket": "splits-cf-templates", @@ -408,7 +427,7 @@ go test -tags=integration ./pkg/operator -v -race -run Test_Operator_Integration "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/infrastructure/aws.go:26" } { - "time": "2025-09-17 14:24:41", + "time": "2025-09-17 15:32:06", "level": "debug", "message": "uploading cloudformation template", "bucket": "splits-cf-templates", @@ -416,7 +435,7 @@ go test -tags=integration ./pkg/operator -v -race -run Test_Operator_Integration "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/infrastructure/aws.go:26" } { - "time": "2025-09-17 14:24:42", + "time": "2025-09-17 15:32:06", "level": "debug", "message": "uploading cloudformation template", "bucket": "splits-cf-templates", @@ -424,7 +443,7 @@ go test -tags=integration ./pkg/operator -v -race -run Test_Operator_Integration "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/infrastructure/aws.go:26" } { - "time": "2025-09-17 14:24:42", + "time": "2025-09-17 15:32:07", "level": "debug", "message": "uploading cloudformation template", "bucket": "splits-cf-templates", @@ -432,7 +451,7 @@ go test -tags=integration ./pkg/operator -v -race -run Test_Operator_Integration "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/infrastructure/aws.go:26" } { - "time": "2025-09-17 14:24:42", + "time": "2025-09-17 15:32:07", "level": "debug", "message": "uploading cloudformation template", "bucket": "splits-cf-templates", @@ -440,7 +459,7 @@ go test -tags=integration ./pkg/operator -v -race -run Test_Operator_Integration "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/infrastructure/aws.go:26" } { - "time": "2025-09-17 14:24:42", + "time": "2025-09-17 15:32:07", "level": "debug", "message": "uploading cloudformation template", "bucket": "splits-cf-templates", @@ -448,7 +467,7 @@ go test -tags=integration ./pkg/operator -v -race -run Test_Operator_Integration "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/infrastructure/aws.go:26" } { - "time": "2025-09-17 14:24:42", + "time": "2025-09-17 15:32:07", "level": "debug", "message": "uploading cloudformation template", "bucket": "splits-cf-templates", @@ -456,7 +475,7 @@ go test -tags=integration ./pkg/operator -v -race -run Test_Operator_Integration "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/infrastructure/aws.go:26" } { - "time": "2025-09-17 14:24:43", + "time": "2025-09-17 15:32:08", "level": "debug", "message": "uploading cloudformation template", "bucket": "splits-cf-templates", @@ -464,7 +483,7 @@ go test -tags=integration ./pkg/operator -v -race -run Test_Operator_Integration "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/infrastructure/aws.go:26" } { - "time": "2025-09-17 14:24:43", + "time": "2025-09-17 15:32:08", "level": "debug", "message": "uploading cloudformation template", "bucket": "splits-cf-templates", @@ -472,7 +491,7 @@ go test -tags=integration ./pkg/operator -v -race -run Test_Operator_Integration "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/infrastructure/aws.go:26" } { - "time": "2025-09-17 14:24:43", + "time": "2025-09-17 15:32:08", "level": "debug", "message": "uploading cloudformation template", "bucket": "splits-cf-templates", @@ -480,7 +499,7 @@ go test -tags=integration ./pkg/operator -v -race -run Test_Operator_Integration "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/infrastructure/aws.go:26" } { - "time": "2025-09-17 14:24:44", + "time": "2025-09-17 15:32:08", "level": "debug", "message": "uploading cloudformation template", "bucket": "splits-cf-templates", @@ -488,16 +507,16 @@ go test -tags=integration ./pkg/operator -v -race -run Test_Operator_Integration "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/infrastructure/aws.go:26" } { - "time": "2025-09-17 14:24:44", + "time": "2025-09-17 15:32:08", "level": "debug", "message": "instrumented worker handler", "handler": "infrastructure", - "latency": "3.811803709s", + "latency": "3.615857209s", "success": "true", "caller": "/Users/xh3b4sd/go/pkg/mod/github.com/0x!splits/workit@v0.6.0/handler/metrics/ensure.go:55" } { - "time": "2025-09-17 14:24:44", + "time": "2025-09-17 15:32:08", "level": "info", "message": "updating cloudformation stack", "name": "server-test", @@ -505,17 +524,17 @@ go test -tags=integration ./pkg/operator -v -race -run Test_Operator_Integration "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/cloudformation/ensure.go:30" } { - "time": "2025-09-17 14:24:44", + "time": "2025-09-17 15:32:08", "level": "debug", "message": "instrumented worker handler", "handler": "cloudformation", - "latency": "419.375µs", + "latency": "190.125µs", "success": "true", "caller": "/Users/xh3b4sd/go/pkg/mod/github.com/0x!splits/workit@v0.6.0/handler/metrics/ensure.go:55" } ---- PASS: Test_Operator_Integration (10.67s) +--- PASS: Test_Operator_Integration (9.93s) PASS -ok github.com/0xSplits/kayron/pkg/operator 11.969s +ok github.com/0xSplits/kayron/pkg/operator 11.224s ``` ### Releases diff --git a/pkg/cache/object.go b/pkg/cache/object.go index 9b41520..9513183 100644 --- a/pkg/cache/object.go +++ b/pkg/cache/object.go @@ -30,6 +30,34 @@ type Object struct { kin kind } +// Domain returns the hash based testing domain for preview deployments, or an +// empty string for any other main release and non-testing environment. +func (o Object) Domain(env string) string { + // Note that we filter the domain name creation by preview deployments, + // because at the time of writing we do not have any convenient way to tell + // whether this release artifact is exposed to the internet via DNS. Right now + // we only know that for certain in case of preview deployments, because their + // sole purpose is to be exposed to the internet. + + if !bool(o.Release.Deploy.Preview) { + return "" + } + + return fmt.Sprintf("%s.%s.%s.splits.org", + o.Release.Labels.Hash.String(), + + // Note that this is a dirty hack to make preview deployments work today for + // existing services that already work using certain incosnistencies between + // repository and domain names. E.g. we have "splits-lite" in Github, but + // use just "lite.testing.splits.org". A better way of doing this would be + // to allow for some kind of domain configuration in the release definition, + // so that we can remove this magical string replacement below. + strings.TrimPrefix(o.Release.Docker.String(), "splits-"), + + env, + ) +} + func (o Object) Name() string { if o.kin == Infrastructure { return o.Release.Github.String() diff --git a/pkg/operator/cloudformation/ensure.go b/pkg/operator/cloudformation/ensure.go index a808a17..a200bf2 100644 --- a/pkg/operator/cloudformation/ensure.go +++ b/pkg/operator/cloudformation/ensure.go @@ -83,16 +83,22 @@ func (c *CloudFormation) temPar(rel []cache.Object) []types.Parameter { } // Inject all desired artifact versions into the parameters that we are just - // about to deploy. Injecting those parameters after all user inputs have been - // applied above guarantees that only the release versions as defined in the - // release source repository will ever be applied. + // about to deploy, but only for main release definitions, not for preview + // deployments. Injecting the template parameters after all user inputs have + // been applied above guarantees that only the release versions as defined in + // the release source repository will ever be applied. for _, x := range rel { + if bool(x.Release.Deploy.Preview) { + continue + } + par = append(par, types.Parameter{ ParameterKey: aws.String(x.Parameter()), ParameterValue: aws.String(x.Version()), }) } + return par } diff --git a/pkg/operator/container/task.go b/pkg/operator/container/task.go index 3ae23b9..3f0201e 100644 --- a/pkg/operator/container/task.go +++ b/pkg/operator/container/task.go @@ -35,6 +35,11 @@ func (c *Container) task(det []detail) ([]task, error) { fnc := func(i int, d detail) error { var err error + // TODO we can make this call more efficient and rate limit friendly by + // fetching all services at once for all injected details. It should then + // not even be necessary anymore to execute this particular code using + // parallel.Slice. + var inp *ecs.DescribeServicesInput { inp = &ecs.DescribeServicesInput{ @@ -60,6 +65,18 @@ func (c *Container) task(det []detail) ([]task, error) { } for _, x := range out.Services { + if aws.ToString(x.Status) != "ACTIVE" { + // There might be inactive or draining services with our desired service + // labels in case we updated CloudFormation stacks multiple times during + // with preview deployments during testing. We only want to consider the + // current state of those stacks that are still active, because the + // inactive versions have most likely been deleted already. + + { + continue + } + } + var pre string var ser string { @@ -129,8 +146,8 @@ func (c *Container) task(det []detail) ([]task, error) { func serTag(tag []types.Tag, key string) string { for _, x := range tag { - if x.Key != nil && x.Value != nil && *x.Key == key { - return *x.Value + if aws.ToString(x.Key) == key { + return aws.ToString(x.Value) } } diff --git a/pkg/operator/operator.go b/pkg/operator/operator.go index 6885a52..3a39b16 100644 --- a/pkg/operator/operator.go +++ b/pkg/operator/operator.go @@ -65,7 +65,7 @@ func New(c Config) *Operator { cloudFormation: cloudformation.New(cloudformation.Config{Aws: c.Aws, Cac: c.Cac, Dry: c.Dry, Env: c.Env, Log: c.Log, Met: c.Met}), container: container.New(container.Config{Aws: c.Aws, Cac: c.Cac, Env: c.Env, Log: c.Log}), infrastructure: infrastructure.New(infrastructure.Config{Aws: c.Aws, Cac: c.Cac, Dry: c.Dry, Env: c.Env, Log: c.Log}), - policy: policy.New(policy.Config{Cac: c.Cac, Log: c.Log}), + policy: policy.New(policy.Config{Cac: c.Cac, Env: c.Env, Log: c.Log}), reference: reference.New(reference.Config{Cac: c.Cac, Env: c.Env, Log: c.Log}), release: release.New(release.Config{Aws: c.Aws, Cac: c.Cac, Env: c.Env, Log: c.Log, Sta: c.Sta}), registry: registry.New(registry.Config{Aws: c.Aws, Cac: c.Cac, Env: c.Env, Log: c.Log}), diff --git a/pkg/operator/operator_integration_test.go b/pkg/operator/operator_integration_test.go index b3e5392..bca1d69 100644 --- a/pkg/operator/operator_integration_test.go +++ b/pkg/operator/operator_integration_test.go @@ -27,6 +27,7 @@ import ( // KAYRON_GITHUB_TOKEN=todo go test -tags=integration ./pkg/operator -v -race -run Test_Operator_Integration func Test_Operator_Integration(t *testing.T) { var env envvar.Env + { env = envvar.Env{ CloudformationStack: "server-test", diff --git a/pkg/operator/policy/ensure.go b/pkg/operator/policy/ensure.go index a09980e..919c32d 100644 --- a/pkg/operator/policy/ensure.go +++ b/pkg/operator/policy/ensure.go @@ -57,21 +57,29 @@ func (p *Policy) ensure(rel []cache.Object) error { // 4. the container image for the desired state must be pushed // + var drf bool + for _, x := range rel { if !bool(x.Release.Deploy.Suspend) && x.Artifact.Drift() && x.Artifact.Valid() { + { + drf = true + } + p.log.Log( "level", "info", "message", "continuing reconciliation loop", "reason", "detected state drift", "release", x.Name(), - "preview", x.Release.Deploy.Preview.String(), + "domain", x.Domain(p.env.Environment), "version", x.Artifact.Reference.Desired, ) - - return nil } } + if drf { + return nil + } + // At this point all service releases were found to be up to date this time // around. This means that we do not have to do any more work for this // particular reconciliation loop. And so we return the control flow error diff --git a/pkg/operator/policy/policy.go b/pkg/operator/policy/policy.go index 7c8b128..b8f24c0 100644 --- a/pkg/operator/policy/policy.go +++ b/pkg/operator/policy/policy.go @@ -7,17 +7,20 @@ import ( "fmt" "github.com/0xSplits/kayron/pkg/cache" + "github.com/0xSplits/kayron/pkg/envvar" "github.com/xh3b4sd/logger" "github.com/xh3b4sd/tracer" ) type Config struct { Cac *cache.Cache + Env envvar.Env Log logger.Interface } type Policy struct { cac *cache.Cache + env envvar.Env log logger.Interface } @@ -25,12 +28,16 @@ func New(c Config) *Policy { if c.Cac == nil { tracer.Panic(tracer.Mask(fmt.Errorf("%T.Cac must not be empty", c))) } + if c.Env.Environment == "" { + tracer.Panic(tracer.Mask(fmt.Errorf("%T.Env must not be empty", c))) + } if c.Log == nil { tracer.Panic(tracer.Mask(fmt.Errorf("%T.Log must not be empty", c))) } return &Policy{ cac: c.Cac, + env: c.Env, log: c.Log, } } diff --git a/pkg/preview/domain.go b/pkg/preview/domain.go deleted file mode 100644 index 5e1b57c..0000000 --- a/pkg/preview/domain.go +++ /dev/null @@ -1,22 +0,0 @@ -package preview - -import ( - "fmt" - "strings" -) - -func preDom(hsh string, doc string) string { - // Note that this is a dirty hack to make preview deployments work today for - // existing services that already work using certain incosnistencies between - // repository and domain names. E.g. we have "splits-lite" in Github, but use - // just "lite.testing.splits.org". A better way of doing this would be to allow - // for some kind of domain configuration in the release definition, so that we - // can remove this magical string replacement below. - - var trm string - { - trm = strings.TrimPrefix(doc, "splits-") - } - - return fmt.Sprintf("%s.%s.${Environment}.splits.org", hsh, trm) -} diff --git a/pkg/preview/render.go b/pkg/preview/render.go index 2eb6f68..8c42389 100644 --- a/pkg/preview/render.go +++ b/pkg/preview/render.go @@ -47,7 +47,7 @@ func (p *Preview) Render(pre []cache.Object) ([]byte, error) { var dom string { - dom = preDom(hsh.String(), x.Release.Docker.String()) + dom = x.Domain("${Environment}") } var ima []byte diff --git a/pkg/stack/search.go b/pkg/stack/search.go index 639b535..43975c2 100644 --- a/pkg/stack/search.go +++ b/pkg/stack/search.go @@ -58,7 +58,7 @@ func (s *Stack) Search() (types.Stack, error) { func hasEnv(tags []types.Tag, env string) bool { for _, x := range tags { - if x.Key != nil && x.Value != nil && *x.Key == "environment" && *x.Value == env { + if aws.ToString(x.Key) == "environment" && aws.ToString(x.Value) == env { return true } } From ceee2cdf8c8815a484663cab701b7ab030c2af67 Mon Sep 17 00:00:00 2001 From: xh3b4sd Date: Wed, 17 Sep 2025 19:03:27 +0200 Subject: [PATCH 08/13] fix --- pkg/preview/render_test.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pkg/preview/render_test.go b/pkg/preview/render_test.go index 8ce80ca..38f4b70 100644 --- a/pkg/preview/render_test.go +++ b/pkg/preview/render_test.go @@ -14,6 +14,7 @@ import ( "github.com/0xSplits/kayron/pkg/release/schema/release" "github.com/0xSplits/kayron/pkg/release/schema/release/deploy" "github.com/0xSplits/kayron/pkg/release/schema/release/deploy/branch" + "github.com/0xSplits/kayron/pkg/release/schema/release/deploy/preview" "github.com/0xSplits/kayron/pkg/release/schema/release/docker" "github.com/0xSplits/kayron/pkg/release/schema/release/labels" "github.com/google/go-cmp/cmp" @@ -34,7 +35,8 @@ func Test_Preview_Render(t *testing.T) { }, Release: release.Struct{ Deploy: deploy.Struct{ - Branch: branch.String("fancy-feature-branch"), + Branch: branch.String("fancy-feature-branch"), + Preview: preview.Bool(true), }, Docker: docker.String("lite"), Labels: labels.Struct{ @@ -50,7 +52,8 @@ func Test_Preview_Render(t *testing.T) { }, Release: release.Struct{ Deploy: deploy.Struct{ - Branch: branch.String("dependabot/another-one"), + Branch: branch.String("dependabot/another-one"), + Preview: preview.Bool(true), }, Docker: docker.String("lite"), Labels: labels.Struct{ From a504e9b59802a77a7093bd9ec260f90fceb6a9a6 Mon Sep 17 00:00:00 2001 From: xh3b4sd Date: Wed, 17 Sep 2025 19:09:07 +0200 Subject: [PATCH 09/13] fix --- pkg/operator/policy/ensure_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/operator/policy/ensure_test.go b/pkg/operator/policy/ensure_test.go index 0729f9d..56f3e36 100644 --- a/pkg/operator/policy/ensure_test.go +++ b/pkg/operator/policy/ensure_test.go @@ -6,6 +6,7 @@ import ( "github.com/0xSplits/kayron/pkg/cache" "github.com/0xSplits/kayron/pkg/cancel" + "github.com/0xSplits/kayron/pkg/envvar" "github.com/0xSplits/kayron/pkg/release/artifact" "github.com/0xSplits/kayron/pkg/release/artifact/condition" "github.com/0xSplits/kayron/pkg/release/artifact/reference" @@ -177,6 +178,9 @@ func Test_Operator_Policy_Ensure(t *testing.T) { { pol = New(Config{ Cac: cac, + Env: envvar.Env{ + Environment: "testing", + }, Log: log, }) } From d2825e222e1a34fb455788d638825f1a3d773d78 Mon Sep 17 00:00:00 2001 From: xh3b4sd Date: Thu, 18 Sep 2025 11:33:13 +0200 Subject: [PATCH 10/13] fix --- README.md | 2 +- pkg/cache/object.go | 2 +- pkg/hash/hash.go | 57 +++++++++++++++++++----- pkg/operator/container/cache.go | 2 +- pkg/preview/expand_test.go | 9 +++- pkg/preview/render.go | 24 +++++----- pkg/preview/testdata/000/out.yaml.golden | 12 ++--- pkg/release/loader/loader_test.go | 11 ++++- 8 files changed, 86 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 8a8204c..7e4f207 100644 --- a/README.md +++ b/README.md @@ -378,7 +378,7 @@ go test -tags=integration ./pkg/operator -v -race -run Test_Operator_Integration "time": "2025-09-17 15:32:05", "level": "info", "message": "continuing reconciliation loop", - "domain": "1D0FD508.lite.testing.splits.org", + "domain": "1d0fd508.lite.testing.splits.org", "reason": "detected state drift", "release": "splits-lite", "version": "e307253e62c2da119579e2505db0b6185642ab9b", diff --git a/pkg/cache/object.go b/pkg/cache/object.go index 9513183..c02acc0 100644 --- a/pkg/cache/object.go +++ b/pkg/cache/object.go @@ -44,7 +44,7 @@ func (o Object) Domain(env string) string { } return fmt.Sprintf("%s.%s.%s.splits.org", - o.Release.Labels.Hash.String(), + o.Release.Labels.Hash.Lower(), // Note that this is a dirty hack to make preview deployments work today for // existing services that already work using certain incosnistencies between diff --git a/pkg/hash/hash.go b/pkg/hash/hash.go index 4284d24..2b175de 100644 --- a/pkg/hash/hash.go +++ b/pkg/hash/hash.go @@ -9,25 +9,62 @@ import ( ) type Hash struct { - Dsh []byte - Hsh []byte + // dsh is the dashed prefix version of Upp. + // + // -1D0FD508 + // + dsh []byte + // low is the lower case version of Upp. + // + // 1d0fd508 + // + low []byte + // upp is the upper case version of this hash. + // + // 1D0FD508 + // + upp []byte } func New(str string) Hash { - sum := sha256.Sum256([]byte(str)) - enc := hex.EncodeToString(sum[:]) - cas := cases.Upper(language.English).String(enc[:8]) + var hsh string + { + hsh = newHsh(str) + } + + var low string + var upp string + { + low = cases.Lower(language.English).String(hsh) + upp = cases.Upper(language.English).String(hsh) + } return Hash{ - Dsh: []byte("-" + cas), - Hsh: []byte(cas), + dsh: []byte("-" + upp), + low: []byte(low), + upp: []byte(upp), } } +func (h Hash) Dashed() string { + return string(h.dsh) +} + func (h Hash) Empty() bool { - return h.Dsh == nil && h.Hsh == nil + return h.dsh == nil && h.low == nil && h.upp == nil } -func (h Hash) String() string { - return string(h.Hsh) +func (h Hash) Lower() string { + return string(h.low) +} + +func (h Hash) Upper() string { + return string(h.upp) +} + +func newHsh(str string) string { + sum := sha256.Sum256([]byte(str)) + enc := hex.EncodeToString(sum[:]) + + return enc[:8] } diff --git a/pkg/operator/container/cache.go b/pkg/operator/container/cache.go index 24a35a4..586ccd8 100644 --- a/pkg/operator/container/cache.go +++ b/pkg/operator/container/cache.go @@ -4,7 +4,7 @@ func (c *Container) cache(ima []image) { for _, x := range c.cac.Services() { var tag string { - tag = curTag(ima, x.Release.Labels.Hash.String(), x.Release.Docker.String()) + tag = curTag(ima, x.Release.Labels.Hash.Upper(), x.Release.Docker.String()) } c.log.Log( diff --git a/pkg/preview/expand_test.go b/pkg/preview/expand_test.go index a36023f..5ff7ee7 100644 --- a/pkg/preview/expand_test.go +++ b/pkg/preview/expand_test.go @@ -140,7 +140,14 @@ func Test_Preview_Expand(t *testing.T) { t.Run(fmt.Sprintf("%03d", i), func(t *testing.T) { exp := expand(tc.rel, tc.pul) - if dif := cmp.Diff(tc.exp, exp); dif != "" { + var opt []cmp.Option + { + opt = []cmp.Option{ + cmp.AllowUnexported(hash.Hash{}), + } + } + + if dif := cmp.Diff(tc.exp, exp, opt...); dif != "" { t.Fatalf("-expected +actual:\n%s", dif) } }) diff --git a/pkg/preview/render.go b/pkg/preview/render.go index 8c42389..5723020 100644 --- a/pkg/preview/render.go +++ b/pkg/preview/render.go @@ -60,7 +60,7 @@ func (p *Preview) Render(pre []cache.Object) ([]byte, error) { var tag []byte { - tag, err = appTag(res.Ser.Search([]byte(" Tags:")).Bytes(), hsh.String()) + tag, err = appTag(res.Ser.Search([]byte(" Tags:")).Bytes(), hsh.Upper()) if err != nil { return nil, tracer.Mask(err) } @@ -85,32 +85,32 @@ func (p *Preview) Render(pre []cache.Object) ([]byte, error) { func (p *Preview) render(res Resource, dom string, pri int, hsh hash.Hash, ima []byte, tag []byte) []byte { { - res.Ser = res.Ser.Append([]byte(" Service:"), hsh.Hsh) - res.Ser = res.Ser.Append([]byte(" ServiceName:"), hsh.Dsh) - res.Ser = res.Ser.Append([]byte(" TaskDefinition:"), hsh.Hsh) - res.Ser = res.Ser.Append([]byte(" - TargetGroupArn:"), hsh.Hsh) + res.Ser = res.Ser.Append([]byte(" Service:"), []byte(hsh.Upper())) + res.Ser = res.Ser.Append([]byte(" ServiceName:"), []byte(hsh.Dashed())) + res.Ser = res.Ser.Append([]byte(" TaskDefinition:"), []byte(hsh.Upper())) + res.Ser = res.Ser.Append([]byte(" - TargetGroupArn:"), []byte(hsh.Upper())) res.Ser = res.Ser.Delete([]byte(" ServiceRegistries:")) res.Ser = res.Ser.Delete([]byte(" Tags:"), tag...) } { - res.Tas = res.Tas.Append([]byte(" TaskDefinition:"), hsh.Hsh) - res.Tas = res.Tas.Append([]byte(" Family:"), hsh.Dsh) + res.Tas = res.Tas.Append([]byte(" TaskDefinition:"), []byte(hsh.Upper())) + res.Tas = res.Tas.Append([]byte(" Family:"), []byte(hsh.Dashed())) res.Tas = res.Tas.Delete([]byte(" Image:"), ima...) } { - res.Dom = res.Dom.Append([]byte(" DomainRecord:"), hsh.Hsh) + res.Dom = res.Dom.Append([]byte(" DomainRecord:"), []byte(hsh.Upper())) res.Dom = res.Dom.Delete([]byte(" Name:"), fmt.Appendf(nil, ` Name: !Sub "%s"`, dom)...) } { - res.Tar = res.Tar.Append([]byte(" TargetGroup:"), hsh.Hsh) + res.Tar = res.Tar.Append([]byte(" TargetGroup:"), []byte(hsh.Upper())) } { - res.Lis = res.Lis.Append([]byte(" ListenerRule:"), hsh.Hsh) - res.Lis = res.Lis.Append([]byte(" TargetGroupArn:"), hsh.Hsh) + res.Lis = res.Lis.Append([]byte(" ListenerRule:"), []byte(hsh.Upper())) + res.Lis = res.Lis.Append([]byte(" TargetGroupArn:"), []byte(hsh.Upper())) res.Lis = res.Lis.Delete([]byte(" Values:"), fmt.Appendf(nil, " Values:\n - !Sub \"%s\"", dom)...) res.Lis = res.Lis.Delete([]byte(" Priority:"), fmt.Appendf(nil, " Priority: %d # Host header = %s", pri, dom)...) } @@ -138,7 +138,7 @@ func header(hsh hash.Hash) []byte { return bytes.Join( [][]byte{ []byte(" #"), - []byte(" # AUTO GENERATED PREVIEW DEPLOYMENT " + hsh.String()), + []byte(" # AUTO GENERATED PREVIEW DEPLOYMENT " + hsh.Upper()), []byte(" #"), }, []byte("\n"), diff --git a/pkg/preview/testdata/000/out.yaml.golden b/pkg/preview/testdata/000/out.yaml.golden index f151eea..50ceb21 100644 --- a/pkg/preview/testdata/000/out.yaml.golden +++ b/pkg/preview/testdata/000/out.yaml.golden @@ -379,7 +379,7 @@ Resources: Properties: HostedZoneId: Fn::ImportValue: !Sub "${NetworkStack}-EnvironmentHostedZoneId" - Name: !Sub "1D0FD508.lite.${Environment}.splits.org" + Name: !Sub "1d0fd508.lite.${Environment}.splits.org" Type: A AliasTarget: DNSName: @@ -413,10 +413,10 @@ Resources: - Field: host-header HostHeaderConfig: Values: - - !Sub "1D0FD508.lite.${Environment}.splits.org" + - !Sub "1d0fd508.lite.${Environment}.splits.org" ListenerArn: Fn::ImportValue: !Sub "${FargateStack}-ApplicationLoadBalancerListenerArn" - Priority: 12001 # Host header = 1D0FD508.lite.${Environment}.splits.org + Priority: 12001 # Host header = 1d0fd508.lite.${Environment}.splits.org # # AUTO GENERATED PREVIEW DEPLOYMENT F4436797 @@ -490,7 +490,7 @@ Resources: Properties: HostedZoneId: Fn::ImportValue: !Sub "${NetworkStack}-EnvironmentHostedZoneId" - Name: !Sub "F4436797.lite.${Environment}.splits.org" + Name: !Sub "f4436797.lite.${Environment}.splits.org" Type: A AliasTarget: DNSName: @@ -524,7 +524,7 @@ Resources: - Field: host-header HostHeaderConfig: Values: - - !Sub "F4436797.lite.${Environment}.splits.org" + - !Sub "f4436797.lite.${Environment}.splits.org" ListenerArn: Fn::ImportValue: !Sub "${FargateStack}-ApplicationLoadBalancerListenerArn" - Priority: 12002 # Host header = F4436797.lite.${Environment}.splits.org + Priority: 12002 # Host header = f4436797.lite.${Environment}.splits.org diff --git a/pkg/release/loader/loader_test.go b/pkg/release/loader/loader_test.go index 0730aeb..19fe82b 100644 --- a/pkg/release/loader/loader_test.go +++ b/pkg/release/loader/loader_test.go @@ -7,6 +7,7 @@ import ( "path/filepath" "testing" + "github.com/0xSplits/kayron/pkg/hash" "github.com/0xSplits/kayron/pkg/release/schema" "github.com/0xSplits/kayron/pkg/release/schema/release" "github.com/0xSplits/kayron/pkg/release/schema/release/deploy" @@ -15,6 +16,7 @@ import ( "github.com/0xSplits/kayron/pkg/release/schema/release/labels" "github.com/0xSplits/kayron/pkg/release/schema/release/provider" "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/spf13/afero" "github.com/xh3b4sd/tracer" ) @@ -244,7 +246,14 @@ func Test_Loader(t *testing.T) { } } - if dif := cmp.Diff(tc.sch, sch); dif != "" { + var opt []cmp.Option + { + opt = []cmp.Option{ + cmpopts.IgnoreUnexported(hash.Hash{}), + } + } + + if dif := cmp.Diff(tc.sch, sch, opt...); dif != "" { t.Fatalf("-expected +actual:\n%s", dif) } }) From a2f526e26eb6ffbaf8b71969030c10fe5de7a7f2 Mon Sep 17 00:00:00 2001 From: xh3b4sd Date: Thu, 18 Sep 2025 16:45:26 +0200 Subject: [PATCH 11/13] fix --- go.mod | 13 ++-- go.sum | 30 ++++---- pkg/cache/update.go | 1 + pkg/operator/chain.go | 6 ++ pkg/operator/cloudformation/active.go | 6 ++ pkg/operator/container/active.go | 6 ++ pkg/operator/container/task.go | 5 -- pkg/operator/infrastructure/active.go | 6 ++ pkg/operator/operator.go | 3 + pkg/operator/policy/active.go | 6 ++ pkg/operator/preview/active.go | 8 ++ pkg/operator/preview/ensure.go | 76 +++++++++++++++++++ pkg/operator/preview/preview.go | 54 +++++++++++++ pkg/operator/reference/active.go | 6 ++ pkg/operator/reference/ensure.go | 9 +-- pkg/operator/reference/github.go | 12 ++- pkg/operator/registry/active.go | 6 ++ pkg/operator/release/active.go | 6 ++ pkg/operator/release/ensure.go | 36 +-------- pkg/operator/release/release.go | 11 --- pkg/operator/template/active.go | 6 ++ pkg/preview/expand.go | 21 ++--- pkg/preview/expand_test.go | 14 ---- pkg/release/schema/release/error.go | 12 +++ pkg/release/schema/release/labels/struct.go | 3 + pkg/release/schema/release/provider/error.go | 19 +++++ pkg/release/schema/release/provider/string.go | 10 ++- pkg/release/schema/release/struct.go | 4 + pkg/release/schema/schema_test.go | 19 +++++ pkg/worker/handler/image/active.go | 6 ++ 30 files changed, 306 insertions(+), 114 deletions(-) create mode 100644 pkg/operator/cloudformation/active.go create mode 100644 pkg/operator/container/active.go create mode 100644 pkg/operator/infrastructure/active.go create mode 100644 pkg/operator/policy/active.go create mode 100644 pkg/operator/preview/active.go create mode 100644 pkg/operator/preview/ensure.go create mode 100644 pkg/operator/preview/preview.go create mode 100644 pkg/operator/reference/active.go create mode 100644 pkg/operator/registry/active.go create mode 100644 pkg/operator/release/active.go create mode 100644 pkg/operator/template/active.go create mode 100644 pkg/release/schema/release/provider/error.go create mode 100644 pkg/worker/handler/image/active.go diff --git a/go.mod b/go.mod index aab732c..9542ff9 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.24.0 require ( github.com/0xSplits/otelgo v0.1.2 github.com/0xSplits/roghfs v0.1.0 - github.com/0xSplits/workit v0.6.0 + github.com/0xSplits/workit v0.7.0 github.com/aws/aws-sdk-go-v2 v1.39.0 github.com/aws/aws-sdk-go-v2/config v1.31.8 github.com/aws/aws-sdk-go-v2/service/cloudformation v1.66.2 @@ -59,7 +59,6 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect @@ -69,20 +68,20 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.66.1 // indirect - github.com/prometheus/otlptranslator v0.0.2 // indirect + github.com/prometheus/otlptranslator v1.0.0 // indirect github.com/prometheus/procfs v0.17.0 // indirect - github.com/puzpuzpuz/xsync/v4 v4.1.0 // indirect + github.com/puzpuzpuz/xsync/v4 v4.2.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/vbatts/tar-split v0.12.1 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel v1.38.0 // indirect go.opentelemetry.io/otel/exporters/prometheus v0.60.0 // indirect go.opentelemetry.io/otel/sdk v1.38.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect go.opentelemetry.io/otel/trace v1.38.0 // indirect - go.yaml.in/yaml/v2 v2.4.2 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect golang.org/x/sync v0.17.0 // indirect golang.org/x/sys v0.36.0 // indirect - google.golang.org/protobuf v1.36.8 // indirect + google.golang.org/protobuf v1.36.9 // indirect ) diff --git a/go.sum b/go.sum index 47655f4..cd8b516 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ github.com/0xSplits/otelgo v0.1.2 h1:QjbUMNNQcUsnkOmZ35bc3Fbhz7u0PA611LYrh4aOpPk github.com/0xSplits/otelgo v0.1.2/go.mod h1:YnmWxWUT7xaMQTF3FBmN8GuvIMafSJ/lYhlyIBlr99w= github.com/0xSplits/roghfs v0.1.0 h1:E3BB8+w+X3g64ezuJ13FssmfNorqy/fx2RGWBFtVifw= github.com/0xSplits/roghfs v0.1.0/go.mod h1:KVlXti9dNWj2YtskpRjZNFqccSKsh5K+UJs73GMXTJs= -github.com/0xSplits/workit v0.6.0 h1:h2LrDdkOTuokSOwt0E3dt8N90fsMsoNd3CCsyI3U0f8= -github.com/0xSplits/workit v0.6.0/go.mod h1:+SQ35oJXLBigYeA1VSrKkJNaX4KBLE+M4PuoBZT1xs0= +github.com/0xSplits/workit v0.7.0 h1:kBa6bJ/9mjUIJYhi+MzBI5kY1iEfSyAYGH+H7wrSMoY= +github.com/0xSplits/workit v0.7.0/go.mod h1:9vuuL6Lr+KfZuOKMihtymd11+8Yq5woBqRqra29gd6U= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/aws/aws-sdk-go-v2 v1.39.0 h1:xm5WV/2L4emMRmMjHFykqiA4M/ra0DJVSWUkDyBjbg4= @@ -88,8 +88,6 @@ 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/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= -github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 h1:cLN4IBkmkYZNnk7EAJ0BHIethd+J6LqxFNw5mSiI2bM= -github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= @@ -122,14 +120,14 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= -github.com/prometheus/otlptranslator v0.0.2 h1:+1CdeLVrRQ6Psmhnobldo0kTp96Rj80DRXRd5OSnMEQ= -github.com/prometheus/otlptranslator v0.0.2/go.mod h1:P8AwMgdD7XEr6QRUJ2QWLpiAZTgTE2UYgjlu3svompI= +github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos= +github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM= github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= -github.com/puzpuzpuz/xsync/v4 v4.1.0 h1:x9eHRl4QhZFIPJ17yl4KKW9xLyVWbb3/Yq4SXpjF71U= -github.com/puzpuzpuz/xsync/v4 v4.1.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/puzpuzpuz/xsync/v4 v4.2.0 h1:dlxm77dZj2c3rxq0/XNvvUKISAmovoXF4a4qM6Wvkr0= +github.com/puzpuzpuz/xsync/v4 v4.2.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -154,8 +152,8 @@ github.com/xh3b4sd/logger v0.11.1 h1:aTK4ygh7aPv1jq54J8bx+zjH6A8RYdkKAgOZYw867C0 github.com/xh3b4sd/logger v0.11.1/go.mod h1:MC7Dp7RC3tZ182KlvSulGcRQVX/D2l+WlCSGLF1mvO8= github.com/xh3b4sd/tracer v1.0.0 h1:mr9uYCx/Ry2w1wdJz0V0Kq71/KeF+hUQjbZQJCxm3Zw= github.com/xh3b4sd/tracer v1.0.0/go.mod h1:nfZeNH5RRfqE6ctQroIfY75b2NRlJHl2g+HP7ddvHrM= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel/exporters/prometheus v0.60.0 h1:cGtQxGvZbnrWdC2GyjZi0PDKVSLWP/Jocix3QWfXtbo= @@ -170,8 +168,8 @@ go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJr go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= -go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -180,8 +178,8 @@ golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= -google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= +google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/pkg/cache/update.go b/pkg/cache/update.go index 2e33c86..5862f2e 100644 --- a/pkg/cache/update.go +++ b/pkg/cache/update.go @@ -12,5 +12,6 @@ func (c *Cache) Update(obj Object) { if obj.kin == Service { c.ser[obj.ind].Artifact = c.ser[obj.ind].Artifact.Merge(obj.Artifact) + c.ser[obj.ind].Release.Deploy.Preview = obj.Release.Deploy.Preview } } diff --git a/pkg/operator/chain.go b/pkg/operator/chain.go index a661fc1..3d16760 100644 --- a/pkg/operator/chain.go +++ b/pkg/operator/chain.go @@ -15,6 +15,12 @@ func (o *Operator) Chain() [][]handler.Ensure { // reconciliation loops. {o.release}, + // Inject any potential preview deployments into our internal list of + // release definitions so that we can render and expose any additional + // development services during testing. Note that this worker handler is + // only active within the testing environment. + {o.preview}, + // Run the next steps in parallel in order to find the current and // desired state of the release artifacts that we are tasked to // managed. diff --git a/pkg/operator/cloudformation/active.go b/pkg/operator/cloudformation/active.go new file mode 100644 index 0000000..a5e86cb --- /dev/null +++ b/pkg/operator/cloudformation/active.go @@ -0,0 +1,6 @@ +package cloudformation + +// Active defines this worker handler to always be executed. +func (c *CloudFormation) Active() bool { + return true +} diff --git a/pkg/operator/container/active.go b/pkg/operator/container/active.go new file mode 100644 index 0000000..b188353 --- /dev/null +++ b/pkg/operator/container/active.go @@ -0,0 +1,6 @@ +package container + +// Active defines this worker handler to always be executed. +func (c *Container) Active() bool { + return true +} diff --git a/pkg/operator/container/task.go b/pkg/operator/container/task.go index 3f0201e..f309a0c 100644 --- a/pkg/operator/container/task.go +++ b/pkg/operator/container/task.go @@ -35,11 +35,6 @@ func (c *Container) task(det []detail) ([]task, error) { fnc := func(i int, d detail) error { var err error - // TODO we can make this call more efficient and rate limit friendly by - // fetching all services at once for all injected details. It should then - // not even be necessary anymore to execute this particular code using - // parallel.Slice. - var inp *ecs.DescribeServicesInput { inp = &ecs.DescribeServicesInput{ diff --git a/pkg/operator/infrastructure/active.go b/pkg/operator/infrastructure/active.go new file mode 100644 index 0000000..89b7066 --- /dev/null +++ b/pkg/operator/infrastructure/active.go @@ -0,0 +1,6 @@ +package infrastructure + +// Active defines this worker handler to always be executed. +func (i *Infrastructure) Active() bool { + return true +} diff --git a/pkg/operator/operator.go b/pkg/operator/operator.go index 3a39b16..c488b36 100644 --- a/pkg/operator/operator.go +++ b/pkg/operator/operator.go @@ -9,6 +9,7 @@ import ( "github.com/0xSplits/kayron/pkg/operator/container" "github.com/0xSplits/kayron/pkg/operator/infrastructure" "github.com/0xSplits/kayron/pkg/operator/policy" + "github.com/0xSplits/kayron/pkg/operator/preview" "github.com/0xSplits/kayron/pkg/operator/reference" "github.com/0xSplits/kayron/pkg/operator/registry" "github.com/0xSplits/kayron/pkg/operator/release" @@ -35,6 +36,7 @@ type Operator struct { container *container.Container infrastructure *infrastructure.Infrastructure policy *policy.Policy + preview *preview.Preview reference *reference.Reference release *release.Release registry *registry.Registry @@ -66,6 +68,7 @@ func New(c Config) *Operator { container: container.New(container.Config{Aws: c.Aws, Cac: c.Cac, Env: c.Env, Log: c.Log}), infrastructure: infrastructure.New(infrastructure.Config{Aws: c.Aws, Cac: c.Cac, Dry: c.Dry, Env: c.Env, Log: c.Log}), policy: policy.New(policy.Config{Cac: c.Cac, Env: c.Env, Log: c.Log}), + preview: preview.New(preview.Config{Cac: c.Cac, Env: c.Env, Log: c.Log}), reference: reference.New(reference.Config{Cac: c.Cac, Env: c.Env, Log: c.Log}), release: release.New(release.Config{Aws: c.Aws, Cac: c.Cac, Env: c.Env, Log: c.Log, Sta: c.Sta}), registry: registry.New(registry.Config{Aws: c.Aws, Cac: c.Cac, Env: c.Env, Log: c.Log}), diff --git a/pkg/operator/policy/active.go b/pkg/operator/policy/active.go new file mode 100644 index 0000000..909e20d --- /dev/null +++ b/pkg/operator/policy/active.go @@ -0,0 +1,6 @@ +package policy + +// Active defines this worker handler to always be executed. +func (p *Policy) Active() bool { + return true +} diff --git a/pkg/operator/preview/active.go b/pkg/operator/preview/active.go new file mode 100644 index 0000000..8b1ce5a --- /dev/null +++ b/pkg/operator/preview/active.go @@ -0,0 +1,8 @@ +package preview + +// Active defines this worker handler to only be executed within the testing +// environment, because we do not allow preview deployments to be injected in +// e.g. staging nor production. +func (p *Preview) Active() bool { + return p.env.Environment == "testing" +} diff --git a/pkg/operator/preview/ensure.go b/pkg/operator/preview/ensure.go new file mode 100644 index 0000000..abb2927 --- /dev/null +++ b/pkg/operator/preview/ensure.go @@ -0,0 +1,76 @@ +package preview + +import ( + "github.com/0xSplits/kayron/pkg/cache" + "github.com/0xSplits/kayron/pkg/release/schema/release" + "github.com/0xSplits/kayron/pkg/release/schema/release/deploy/preview" + "github.com/xh3b4sd/choreo/parallel" + "github.com/xh3b4sd/tracer" +) + +func (p *Preview) Ensure() error { + // Get the list of cached releases so that we can lookup their respective + // artifact references for any potential preview deployment settings. + + var rel []cache.Object + { + rel = p.cac.Releases() + } + + fnc := func(_ int, o cache.Object) error { + var err error + + // If this release has preview deployments disabled, then ignore this cache + // object and move on to the next one. + + if !bool(o.Release.Deploy.Preview) { + return nil + } + + // If this release has preview deployments enabled, then compute the preview + // releases, so that we can inject them into the internal cache below. + + var exp release.Slice + { + exp, err = p.pre.Expand(o.Release) + if err != nil { + return tracer.Mask(err) + } + } + + // Extend the cache for all expanded preview deployments. + + { + err := p.cac.Create(exp) + if err != nil { + return tracer.Mask(err) + } + } + + // Mark the expanded release artifact as non-preview. The Deploy.Preview + // flag of the release.Struct acts as a signal to expand our release + // definitions internally. Once expanded, we redefine the purpose of this + // preview flag to maintain our understanding of how to deploy "real" + // service releases. In other words, we turn one release into many, while + // muting the one that instructed the many for the preview mechanism. + + { + o.Release.Deploy.Preview = preview.Bool(false) + } + + { + p.cac.Update(o) + } + + return nil + } + + { + err := parallel.Slice(rel, fnc) + if err != nil { + return tracer.Mask(err) + } + } + + return nil +} diff --git a/pkg/operator/preview/preview.go b/pkg/operator/preview/preview.go new file mode 100644 index 0000000..8ed4434 --- /dev/null +++ b/pkg/operator/preview/preview.go @@ -0,0 +1,54 @@ +// Package preview injects preview deployments into our internal release +// artifact cache. This operator function enables us to expose additional +// development services within the testing environment only. +package preview + +import ( + "fmt" + + "github.com/0xSplits/kayron/pkg/cache" + "github.com/0xSplits/kayron/pkg/envvar" + "github.com/0xSplits/kayron/pkg/preview" + "github.com/xh3b4sd/logger" + "github.com/xh3b4sd/tracer" +) + +type Config struct { + Cac *cache.Cache + Env envvar.Env + Log logger.Interface +} + +type Preview struct { + cac *cache.Cache + env envvar.Env + log logger.Interface + pre *preview.Preview +} + +func New(c Config) *Preview { + if c.Cac == nil { + tracer.Panic(tracer.Mask(fmt.Errorf("%T.Cac must not be empty", c))) + } + if c.Env.Environment == "" { + tracer.Panic(tracer.Mask(fmt.Errorf("%T.Env must not be empty", c))) + } + if c.Log == nil { + tracer.Panic(tracer.Mask(fmt.Errorf("%T.Log must not be empty", c))) + } + + var pre *preview.Preview + { + pre = preview.New(preview.Config{ + Env: c.Env, + Inp: []byte{}, + }) + } + + return &Preview{ + cac: c.Cac, + env: c.Env, + log: c.Log, + pre: pre, + } +} diff --git a/pkg/operator/reference/active.go b/pkg/operator/reference/active.go new file mode 100644 index 0000000..a8b85ed --- /dev/null +++ b/pkg/operator/reference/active.go @@ -0,0 +1,6 @@ +package reference + +// Active defines this worker handler to always be executed. +func (r *Reference) Active() bool { + return true +} diff --git a/pkg/operator/reference/ensure.go b/pkg/operator/reference/ensure.go index 2519471..3192fcf 100644 --- a/pkg/operator/reference/ensure.go +++ b/pkg/operator/reference/ensure.go @@ -18,18 +18,11 @@ func (r *Reference) Ensure() error { // Find the reference for every branch deployment strategy. The concurrently // executed function below prevents network calls for every release that does - // not define a branch deployment strategy. Note that we also do not lookup - // branch references for preview deployments, if those references do already - // exist. E.g. we may have looked up the latest commit sha for a preview - // deployment in an earlier stage of this reconciliation loop. + // not define a branch deployment strategy. fnc := func(i int, x cache.Object) error { var err error - if bool(x.Release.Deploy.Preview) && x.Artifact.Reference.Desired != "" { - return nil - } - var ref string { ref, err = r.desRef(x.Release) diff --git a/pkg/operator/reference/github.go b/pkg/operator/reference/github.go index 264d0d6..c61679c 100644 --- a/pkg/operator/reference/github.go +++ b/pkg/operator/reference/github.go @@ -10,10 +10,18 @@ import ( func (r *Reference) desRef(rel release.Struct) (string, error) { // Return the commit sha if the branch deployment strategy is selected. Note // that branches may be referenced in releases while they are not yet tracked, - // or not tracked anymore inside of Github. This may happen predominantly - // during testing when preparing or finishing releases and their dependencies. + // or not tracked anymore inside of Github. This may happen predominantly during + // testing when preparing or finishing releases and their dependencies. Note + // that we do not lookup branch references for preview deployments, if those + // references got already filled in the release labels. E.g. we may have looked + // up the latest commit sha for a preview deployment in an earlier stage of this + // reconciliation loop already. if !rel.Deploy.Branch.Empty() { + if bool(rel.Deploy.Preview) && rel.Labels.Head != "" { + return rel.Labels.Head, nil + } + sha, err := r.comSha(rel.Github.String(), rel.Deploy.Branch.String()) if err != nil { return "", tracer.Mask(err) diff --git a/pkg/operator/registry/active.go b/pkg/operator/registry/active.go new file mode 100644 index 0000000..aeaa41f --- /dev/null +++ b/pkg/operator/registry/active.go @@ -0,0 +1,6 @@ +package registry + +// Active defines this worker handler to always be executed. +func (r *Registry) Active() bool { + return true +} diff --git a/pkg/operator/release/active.go b/pkg/operator/release/active.go new file mode 100644 index 0000000..97d4401 --- /dev/null +++ b/pkg/operator/release/active.go @@ -0,0 +1,6 @@ +package release + +// Active defines this worker handler to always be executed. +func (r *Release) Active() bool { + return true +} diff --git a/pkg/operator/release/ensure.go b/pkg/operator/release/ensure.go index 076d834..a11064b 100644 --- a/pkg/operator/release/ensure.go +++ b/pkg/operator/release/ensure.go @@ -6,7 +6,6 @@ import ( "github.com/0xSplits/kayron/pkg/operator/release/resolver" "github.com/0xSplits/kayron/pkg/release/loader" "github.com/0xSplits/kayron/pkg/release/schema" - "github.com/0xSplits/kayron/pkg/release/schema/release" "github.com/0xSplits/roghfs" "github.com/spf13/afero" "github.com/xh3b4sd/tracer" @@ -111,48 +110,15 @@ func (r *Release) Ensure() error { } } - var rel release.Slice - - // TODO run in parallel - for _, x := range sch.Release { - // If this release has preview deployments enabled, then compute the preview - // releases and inject them into the new list of release definitions. - - if x.Deploy.Preview { - exp, err := r.pre.Expand(x) - if err != nil { - return tracer.Mask(err) - } - - { - rel = append(rel, exp...) - } - } - - // If this release has preview deployments disabled, then only track this - // main release in the new list of release definitions. - if !x.Deploy.Preview { - { - rel = append(rel, x) - } - } - } - // Initialize the cache for all configured releases regardless of their type. // Here we require exactly one infrastructure release to be provided. { - err = r.cac.Create(rel) + err = r.cac.Create(sch.Release) if err != nil { return tracer.Mask(err) } } - // TODO there is a potential efficiency benefit to be had if we expanded cache - // objects instead of release definitions, because we are already fetching the - // latest Git SHAs for every preview deployment in Preview.Expand above. With - // that we should probably also move all of this cache object expansion for - // preview deployments into its own operator handler/function. - return nil } diff --git a/pkg/operator/release/release.go b/pkg/operator/release/release.go index 96c272d..6916934 100644 --- a/pkg/operator/release/release.go +++ b/pkg/operator/release/release.go @@ -11,7 +11,6 @@ import ( "github.com/0xSplits/kayron/pkg/envvar" "github.com/0xSplits/kayron/pkg/operator/release/canceler" "github.com/0xSplits/kayron/pkg/operator/release/resolver" - "github.com/0xSplits/kayron/pkg/preview" "github.com/0xSplits/kayron/pkg/stack" "github.com/0xSplits/roghfs" "github.com/aws/aws-sdk-go-v2/aws" @@ -35,7 +34,6 @@ type Release struct { git *github.Client log logger.Interface own string - pre *preview.Preview rep string res resolver.Interface sta stack.Interface @@ -83,14 +81,6 @@ func New(c Config) *Release { }) } - var pre *preview.Preview - { - pre = preview.New(preview.Config{ - Env: c.Env, - Inp: []byte{}, - }) - } - var res resolver.Interface { res = resolver.New(resolver.Config{ @@ -107,7 +97,6 @@ func New(c Config) *Release { git: git, log: c.Log, own: own, - pre: pre, rep: rep, res: res, sta: c.Sta, diff --git a/pkg/operator/template/active.go b/pkg/operator/template/active.go new file mode 100644 index 0000000..4f40bec --- /dev/null +++ b/pkg/operator/template/active.go @@ -0,0 +1,6 @@ +package template + +// Active defines this worker handler to always be executed. +func (t *Template) Active() bool { + return true +} diff --git a/pkg/preview/expand.go b/pkg/preview/expand.go index 7fcf4a1..45f57c4 100644 --- a/pkg/preview/expand.go +++ b/pkg/preview/expand.go @@ -71,22 +71,7 @@ func expand(rel release.Struct, pul []*github.PullRequest) release.Slice { return fil[i].GetCreatedAt().Before(fil[j].GetCreatedAt().Time) }) - // Mark the expanded service release as non-preview. The Deploy.Preview - // flag acts as a signal to expand our release definitions internally. - // Once expanded, we redefine the purpose of this preview flag to maintain - // our understanding of how to deploy "real" service releases. In other - // words, we turn one release into many, while muting the one that - // instructed the many for the preview mechanism. - - { - rel.Deploy.Preview = preview.Bool(false) - } - var lis release.Slice - { - lis = append(lis, rel) - } - for _, x := range fil { var pre release.Struct { @@ -94,8 +79,10 @@ func expand(rel release.Struct, pul []*github.PullRequest) release.Slice { } var bra string + var ref string { bra = x.GetHead().GetRef() + ref = x.GetHead().GetSHA() } { @@ -108,9 +95,13 @@ func expand(rel release.Struct, pul []*github.PullRequest) release.Slice { // Make sure to inject the preview deployment hash into the release labels. // This is used to identify the correct current state of deployed container // image tags, as well as rendering the correct CloudFormation templates. + // Here we also optimize the branch reference lookup by assigning the head + // label, because we have this latest commit sha for the preview branch here + // already. { pre.Labels.Hash = hash.New(bra) + pre.Labels.Head = ref } { diff --git a/pkg/preview/expand_test.go b/pkg/preview/expand_test.go index 5ff7ee7..73fecad 100644 --- a/pkg/preview/expand_test.go +++ b/pkg/preview/expand_test.go @@ -37,13 +37,6 @@ func Test_Preview_Expand(t *testing.T) { {CreatedAt: tesTim(5), Head: tesBra("b/5")}, }, exp: release.Slice{ - { - Deploy: deploy.Struct{ - Branch: branch.String("main"), - Preview: preview.Bool(false), - }, - Docker: docker.String("lite"), - }, { Deploy: deploy.Struct{ Branch: branch.String("b/3"), @@ -85,13 +78,6 @@ func Test_Preview_Expand(t *testing.T) { {CreatedAt: tesTim(9), Head: tesBra("b/9")}, }, exp: release.Slice{ - { - Deploy: deploy.Struct{ - Branch: branch.String("main"), - Preview: preview.Bool(false), - }, - Docker: docker.String("lite"), - }, { Deploy: deploy.Struct{ Branch: branch.String("b/3"), diff --git a/pkg/release/schema/release/error.go b/pkg/release/schema/release/error.go index dee57c4..7b8d703 100644 --- a/pkg/release/schema/release/error.go +++ b/pkg/release/schema/release/error.go @@ -33,6 +33,18 @@ func IsServiceDeployEmpty(err error) bool { // // +var releaseDeployPreviewError = &tracer.Error{ + Description: "The release configuration does not allow preview deployments for infrastructure providers.", +} + +func IsServiceDeployPreview(err error) bool { + return errors.Is(err, releaseDeployPreviewError) +} + +// +// +// + var releaseGithubEmptyError = &tracer.Error{ Description: "The release configuration requires a github repository to be provided.", } diff --git a/pkg/release/schema/release/labels/struct.go b/pkg/release/schema/release/labels/struct.go index 7a1b691..416fe6c 100644 --- a/pkg/release/schema/release/labels/struct.go +++ b/pkg/release/schema/release/labels/struct.go @@ -14,6 +14,9 @@ type Struct struct { // Hash contains the hashed branch name for any service release of a preview // deployment. Hash hash.Hash + // Head is the latest Git Reference for any service release of a preview + // deployment. + Head string // Source is the absolute source file path of the .yaml definition as loaded // from the underlying file system. This label may help to make error messages // more useful. diff --git a/pkg/release/schema/release/provider/error.go b/pkg/release/schema/release/provider/error.go new file mode 100644 index 0000000..f907279 --- /dev/null +++ b/pkg/release/schema/release/provider/error.go @@ -0,0 +1,19 @@ +package provider + +import ( + "errors" + + "github.com/xh3b4sd/tracer" +) + +// +// +// + +var providerNameError = &tracer.Error{ + Description: "The provider configuration requires the provider name to be \"cloudformation\".", +} + +func IsProviderName(err error) bool { + return errors.Is(err, providerNameError) +} diff --git a/pkg/release/schema/release/provider/string.go b/pkg/release/schema/release/provider/string.go index 101c72a..4fe8887 100644 --- a/pkg/release/schema/release/provider/string.go +++ b/pkg/release/schema/release/provider/string.go @@ -1,5 +1,10 @@ package provider +import ( + "github.com/0xSplits/kayron/pkg/constant" + "github.com/xh3b4sd/tracer" +) + type String string func (s String) Empty() bool { @@ -11,6 +16,9 @@ func (s String) String() string { } func (s String) Verify() error { - // TODO + if s != constant.Cloudformation { + return tracer.Mask(providerNameError) + } + return nil } diff --git a/pkg/release/schema/release/struct.go b/pkg/release/schema/release/struct.go index 62c736b..1f448d8 100644 --- a/pkg/release/schema/release/struct.go +++ b/pkg/release/schema/release/struct.go @@ -72,6 +72,10 @@ func (s Struct) verify() error { } if !s.Provider.Empty() { + if bool(s.Deploy.Preview) { + return tracer.Mask(releaseDeployPreviewError) + } + err := s.Provider.Verify() if err != nil { return tracer.Mask(err) diff --git a/pkg/release/schema/schema_test.go b/pkg/release/schema/schema_test.go index 943ba49..8aff951 100644 --- a/pkg/release/schema/schema_test.go +++ b/pkg/release/schema/schema_test.go @@ -6,6 +6,7 @@ import ( "github.com/0xSplits/kayron/pkg/release/schema/release" "github.com/0xSplits/kayron/pkg/release/schema/release/deploy" + "github.com/0xSplits/kayron/pkg/release/schema/release/deploy/preview" "github.com/0xSplits/kayron/pkg/release/schema/release/deploy/webhook" "github.com/0xSplits/kayron/pkg/release/schema/release/docker" "github.com/0xSplits/kayron/pkg/release/schema/release/github" @@ -191,6 +192,24 @@ func Test_Schema_Verify_failure(t *testing.T) { }, mat: release.IsServiceLabelsEmpty, }, + // Case 009, one provider, preview deployments + { + sch: Schema{ + Release: release.Slice{ + { + Github: github.String("infrastructure"), + Provider: provider.String("cloudformation"), + Deploy: deploy.Struct{ + Preview: preview.Bool(true), + }, + Labels: labels.Struct{ + Source: "foo", + }, + }, + }, + }, + mat: release.IsServiceDeployPreview, + }, } for i, tc := range testCases { diff --git a/pkg/worker/handler/image/active.go b/pkg/worker/handler/image/active.go new file mode 100644 index 0000000..1d4fbb4 --- /dev/null +++ b/pkg/worker/handler/image/active.go @@ -0,0 +1,6 @@ +package image + +// Active defines this worker handler to always be executed. +func (h *Handler) Active() bool { + return true +} From c4719b62c3348361abb1e4f48f8fae5d29473ee0 Mon Sep 17 00:00:00 2001 From: xh3b4sd Date: Thu, 18 Sep 2025 17:18:29 +0200 Subject: [PATCH 12/13] fix --- pkg/release/schema/release/labels/struct.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/release/schema/release/labels/struct.go b/pkg/release/schema/release/labels/struct.go index 416fe6c..64f8f88 100644 --- a/pkg/release/schema/release/labels/struct.go +++ b/pkg/release/schema/release/labels/struct.go @@ -24,7 +24,7 @@ type Struct struct { } func (s Struct) Empty() bool { - return s.Block == 0 && s.Hash.Empty() && s.Source == "" + return s.Block == 0 && s.Hash.Empty() && s.Head == "" && s.Source == "" } func (s Struct) Verify() error { From 1aeb7bde1db0c953a09df38727e850d5631fe1aa Mon Sep 17 00:00:00 2001 From: xh3b4sd Date: Fri, 19 Sep 2025 12:09:31 +0200 Subject: [PATCH 13/13] fix --- .github/assets/Operator-Design.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/assets/Operator-Design.svg b/.github/assets/Operator-Design.svg index 813363c..966d50e 100644 --- a/.github/assets/Operator-Design.svg +++ b/.github/assets/Operator-Design.svg @@ -1,4 +1,4 @@ -KayronGithubinfrastructurereleasecontainerreferencev1.8.3v1.9.0ECSGithubstagingtestingCloudFormationrdsecsvpcregistrypolicynetworkcachescopev1.8.3v1.9.0ec2cloudformationtemplate \ No newline at end of file +Kayroninfrastructurepreviewcontainerreferenceregistrypolicycachescopecloudformationtemplaterelease \ No newline at end of file