diff --git a/Makefile b/Makefile index 7823aa6d..f7a5c5a2 100644 --- a/Makefile +++ b/Makefile @@ -109,6 +109,12 @@ build: docs: go run ./docs/ +.PHONY: validate +validate: + dagger call validate \ + --one-password-service-account env:OP_SERVICE_ACCOUNT \ + --progress plain + .PHONE: release release: dagger call release \ diff --git a/dagger.json b/dagger.json index ed4f7f62..4ff17a95 100644 --- a/dagger.json +++ b/dagger.json @@ -2,6 +2,10 @@ "name": "replicated", "sdk": "go", "dependencies": [ + { + "name": "aws-sdk", + "source": "github.com/ernesto27/daggerverse/aws-sdk@4921d3afbdd99bc24a214d9a88c6ed8b50adca6c" + }, { "name": "goreleaser", "source": "github.com/developer-guy/excoriate-daggerverse/goreleaser@b4bc20faec104b7f45c9a429b20f7d39d1531b79" diff --git a/dagger/compatibility.go b/dagger/compatibility.go index 457862ad..9dfe8c55 100644 --- a/dagger/compatibility.go +++ b/dagger/compatibility.go @@ -10,6 +10,6 @@ func validateCompatibility( // +defaultPath="./" source *dagger.Directory, -) error { - return nil +) (bool, map[string]Logs, error) { + return true, map[string]Logs{}, nil } diff --git a/dagger/functionality.go b/dagger/functionality.go index 5db04668..6c41676c 100644 --- a/dagger/functionality.go +++ b/dagger/functionality.go @@ -10,10 +10,12 @@ func validateFunctionality( // +defaultPath="./" source *dagger.Directory, -) error { +) (bool, map[string]Logs, error) { goModCache := dag.CacheVolume("replicated-go-mod-122") goBuildCache := dag.CacheVolume("replicated-go-build-121") + checkLogs := map[string]Logs{} + // unit tests unitTest := dag.Container(). From("golang:1.22"). @@ -25,10 +27,18 @@ func validateFunctionality( WithEnvVariable("GOCACHE", "/go/build-cache"). With(CacheBustingExec([]string{"make", "test-unit"})) - _, err := unitTest.Stderr(ctx) + unitTestStdout, err := unitTest.Stdout(ctx) + if err != nil { + return false, nil, err + } + unitTestStderr, err := unitTest.Stderr(ctx) if err != nil { - return err + return false, nil, err + } + checkLogs["unit-tests"] = Logs{ + Stdout: unitTestStdout, + Stderr: unitTestStderr, } - return nil + return true, checkLogs, nil } diff --git a/dagger/go.mod b/dagger/go.mod index 41a97114..de7b23a1 100644 --- a/dagger/go.mod +++ b/dagger/go.mod @@ -21,6 +21,8 @@ require ( google.golang.org/grpc v1.65.0 ) +require github.com/aws/aws-sdk-go v1.55.5 // indirect + require ( github.com/Masterminds/semver v1.5.0 github.com/cenkalti/backoff/v4 v4.3.0 // indirect diff --git a/dagger/go.sum b/dagger/go.sum index 3c78b01f..40998189 100644 --- a/dagger/go.sum +++ b/dagger/go.sum @@ -6,6 +6,8 @@ github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3Q github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU= +github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= diff --git a/dagger/performance.go b/dagger/performance.go index 5a813263..6b39d6c5 100644 --- a/dagger/performance.go +++ b/dagger/performance.go @@ -10,6 +10,6 @@ func validatePerformance( // +defaultPath="./" source *dagger.Directory, -) error { - return nil +) (bool, map[string]Logs, error) { + return true, map[string]Logs{}, nil } diff --git a/dagger/security.go b/dagger/security.go index 55cdd06c..49dbffe1 100644 --- a/dagger/security.go +++ b/dagger/security.go @@ -10,10 +10,12 @@ func validateSecurity( // +defaultPath="./" source *dagger.Directory, -) error { +) (bool, map[string]Logs, error) { goModCache := dag.CacheVolume("replicated-go-mod-122") goBuildCache := dag.CacheVolume("replicated-go-build-121") + checkLogs := map[string]Logs{} + // run semgrep semgrep := dag.Container(). From("returntocorp/semgrep"). @@ -25,10 +27,18 @@ func validateSecurity( WithEnvVariable("GOCACHE", "/go/build-cache"). With(CacheBustingExec([]string{"semgrep", "scan", "--config=p/golang", "."})) - _, err := semgrep.Stderr(ctx) + semgrepStdout, err := semgrep.Stdout(ctx) + if err != nil { + return false, nil, err + } + semgrepStderr, err := semgrep.Stderr(ctx) if err != nil { - return err + return false, nil, err + } + checkLogs["semgrep"] = Logs{ + Stdout: semgrepStdout, + Stderr: semgrepStderr, } - return nil + return true, checkLogs, nil } diff --git a/dagger/validate.go b/dagger/validate.go index e1807e55..4f432d56 100644 --- a/dagger/validate.go +++ b/dagger/validate.go @@ -1,31 +1,173 @@ package main import ( + "bytes" "context" "dagger/replicated/internal/dagger" + "encoding/json" + "fmt" + "os" + "strings" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/request" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/s3" ) +type Logs struct { + Stdout string `json:"stdout"` + Stderr string `json:"stderr"` +} + +type ValidationResult struct { + IsSecurityPass bool `json:"isSecurityPass"` + SecurityLogs map[string]Logs `json:"securityLogs"` + + IsFunctionalityPass bool `json:"isFunctionalityPass"` + FunctionalityLogs map[string]Logs `json:"functionalityLogs"` + + IsCompatibilityPass bool `json:"isCompatibilityPass"` + CompatibilityLogs map[string]Logs `json:"compatibilityLogs"` + + IsPerformancePass bool `json:"isPerformancePass"` + PerformanceLogs map[string]Logs `json:"performanceLogs"` +} + func (r *Replicated) Validate( ctx context.Context, // +defaultPath="./" source *dagger.Directory, + + // +optional + onePasswordServiceAccount *dagger.Secret, ) error { - if err := validateSecurity(ctx, source); err != nil { + validationResult := ValidationResult{} + + securityOK, securityLogs, err := validateSecurity(ctx, source) + if err != nil { + return err + } + validationResult.IsSecurityPass = securityOK + validationResult.SecurityLogs = securityLogs + + functionalityOK, functionalityLogs, err := validateFunctionality(ctx, source) + if err != nil { + return err + } + validationResult.IsFunctionalityPass = functionalityOK + validationResult.FunctionalityLogs = functionalityLogs + + compatibilityOK, copmfunctionalityLogs, err := validateCompatibility(ctx, source) + if err != nil { + return err + } + validationResult.IsCompatibilityPass = compatibilityOK + validationResult.CompatibilityLogs = copmfunctionalityLogs + + performanceOK, performanceLogs, err := validatePerformance(ctx, source) + if err != nil { + return err + } + validationResult.IsPerformancePass = performanceOK + validationResult.PerformanceLogs = performanceLogs + + if onePasswordServiceAccount != nil { + container := dag.Container(). + From("alpine/git:latest"). + WithMountedDirectory("/go/src/github.com/replicatedhq/replicated", source). + WithWorkdir("/go/src/github.com/replicatedhq/replicated"). + With(CacheBustingExec([]string{"git", "status", "--porcelain"})) + + gitStatusOutput, err := container.Stdout(ctx) + if err != nil { + return err + } + + gitTreeClean := len(strings.TrimSpace(gitStatusOutput)) == 0 + + container = dag.Container(). + From("alpine/git:latest"). + WithMountedDirectory("/go/src/github.com/replicatedhq/replicated", source). + WithWorkdir("/go/src/github.com/replicatedhq/replicated"). + With(CacheBustingExec([]string{"git", "rev-parse", "HEAD"})) + + commit, err := container.Stdout(ctx) + if err != nil { + return err + } + commit = strings.TrimSpace(commit) + + if gitTreeClean { + if err := uploadValidationResult(ctx, onePasswordServiceAccount, validationResult, commit); err != nil { + return err + } + } + } + + return nil +} + +func uploadValidationResult(ctx context.Context, onePasswordServiceAccount *dagger.Secret, validationResult ValidationResult, commit string) error { + b, err := json.Marshal(validationResult) + if err != nil { return err } - if err := validateFunctionality(ctx, source); err != nil { + // i think i need to create a new container to write b to a dagger.File? + f := dag.Directory(). + WithNewFile("validation-result.json", string(b)). + File("validation-result.json") + f = f.WithName(commit) + accessKeyID, err := dag.Onepassword().FindSecret( + onePasswordServiceAccount, + "Developer Automation", + "S3 Workflow Validation", + "access_key_id").Plaintext(ctx) + if err != nil { return err } - if err := validateCompatibility(ctx, source); err != nil { + secretAccessKey, err := dag.Onepassword().FindSecret( + onePasswordServiceAccount, + "Developer Automation", + "S3 Workflow Validation", + "secret_access_key").Plaintext(ctx) + if err != nil { return err } - if err := validatePerformance(ctx, source); err != nil { + sess := session.Must(session.NewSession(&aws.Config{ + Region: aws.String("us-east-1"), + Credentials: credentials.NewStaticCredentials(accessKeyID, secretAccessKey, ""), + })) + + svc := s3.New(sess) + f.Export(ctx, commit) + + readFile, err := os.ReadFile(commit) + if err != nil { return err } + _, err = svc.PutObjectWithContext(ctx, &s3.PutObjectInput{ + Bucket: aws.String("workflow-validation-results"), + Body: bytes.NewReader(readFile), + Key: aws.String(commit), + ContentDisposition: aws.String("attachment"), + }) + if err != nil { + if aerr, ok := err.(awserr.Error); ok && aerr.Code() == request.CanceledErrorCode { + fmt.Fprintf(os.Stderr, "upload canceled due to timeout, %v\n", err) + return err + } else { + fmt.Fprintf(os.Stderr, "failed to upload object, %v\n", err) + return err + } + } + return nil }