diff --git a/cmd/appstreamfile/main.go b/cmd/appstreamfile/main.go index 1a22c0d..124592f 100644 --- a/cmd/appstreamfile/main.go +++ b/cmd/appstreamfile/main.go @@ -11,45 +11,76 @@ import ( "github.com/aslamcodes/appstreamfile/internal/validator" ) +type RunOptions struct { + location string + bucket string + key string + versionId string +} + func main() { - source := flag.String("source", "", "The source to pick actions from") - location := flag.String("location", "", "The config file location") + source := flag.String("source", "", "Configuration source: s3 or local") + location := flag.String("location", "", "Local filesystem path to the config file") + bucket := flag.String("bucket", "", "S3 bucket containing the config file") + key := flag.String("key", "", "S3 object key for the config file") + versionId := flag.String("version-id", "", "Optional S3 object version ID") flag.Parse() logger.Init() - if err := run(*source, *location); err != nil { + runOptions := &RunOptions{ + location: *location, + bucket: *bucket, + key: *key, + versionId: *versionId, + } + + if err := run(*source, runOptions); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } -func run(sourceType string, location string) error { +func run(sourceType string, opts *RunOptions) error { + var backendSource backend.BackendSource + var err error + switch sourceType { - case "local": - backend := backend.LocalBackend{ - Location: location, + case "local": + if opts.location == "" { + return fmt.Errorf("location of config file must be provided") } + backendSource, err = backend.NewLocalBackend(opts.location) + case "s3": + if opts.bucket == "" || opts.key == "" { + return fmt.Errorf("missing required S3 options: bucket and key") + } + backendSource, err = backend.NewS3Backend(opts.bucket, opts.key, opts.versionId, "appstream_machine_role") - config, err := backend.GetConfig() - if err != nil { - return fmt.Errorf("failed to fetch config from backend: %w", err) - } + default: + return fmt.Errorf("invalid source provided") + } - if err := validator.ValidateConfig(config); err != nil { - return fmt.Errorf("config file validation failed: %w", err) - } + if err != nil { + return fmt.Errorf("unable to create backend source: %w", err) + } - err = service.ImplementConfig(config) + config, err := backendSource.GetConfig() - if err != nil { - return fmt.Errorf("error setting up config: %w", err) - } + if err != nil { + return fmt.Errorf("failed to fetch config from backend: %w", err) + } - default: - return fmt.Errorf("invalid source provided") + if err := validator.ValidateConfig(config); err != nil { + return fmt.Errorf("config file validation failed: %w", err) + } + + err = service.ImplementConfig(config) + + if err != nil { + return fmt.Errorf("error setting up config: %w", err) } return nil diff --git a/go.mod b/go.mod index d41053c..190338b 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,28 @@ module github.com/aslamcodes/appstreamfile go 1.24.1 -require github.com/goccy/go-yaml v1.19.0 +require ( + github.com/aws/aws-sdk-go-v2/config v1.32.5 + github.com/aws/aws-sdk-go-v2/service/s3 v1.94.0 + github.com/goccy/go-yaml v1.19.0 +) + +require ( + github.com/aws/aws-sdk-go-v2 v1.41.0 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.19.5 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.7 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 // indirect + github.com/aws/smithy-go v1.24.0 // indirect +) diff --git a/go.sum b/go.sum index 22b522d..a31e2e3 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,40 @@ +github.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgPKd4= +github.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4= +github.com/aws/aws-sdk-go-v2/config v1.32.5 h1:pz3duhAfUgnxbtVhIK39PGF/AHYyrzGEyRD9Og0QrE8= +github.com/aws/aws-sdk-go-v2/config v1.32.5/go.mod h1:xmDjzSUs/d0BB7ClzYPAZMmgQdrodNjPPhd6bGASwoE= +github.com/aws/aws-sdk-go-v2/credentials v1.19.5 h1:xMo63RlqP3ZZydpJDMBsH9uJ10hgHYfQFIk1cHDXrR4= +github.com/aws/aws-sdk-go-v2/credentials v1.19.5/go.mod h1:hhbH6oRcou+LpXfA/0vPElh/e0M3aFeOblE1sssAAEk= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 h1:80+uETIWS1BqjnN9uJ0dBUaETh+P1XwFy5vwHwK5r9k= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16/go.mod h1:wOOsYuxYuB/7FlnVtzeBYRcjSRtQpAW0hCP7tIULMwo= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 h1:rgGwPzb82iBYSvHMHXc8h9mRoOUBZIGFgKb9qniaZZc= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16/go.mod h1:L/UxsGeKpGoIj6DxfhOWHWQ/kGKcd4I1VncE4++IyKA= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 h1:1jtGzuV7c82xnqOVfx2F0xmJcOw5374L7N6juGW6x6U= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16/go.mod h1:M2E5OQf+XLe+SZGmmpaI2yy+J326aFf6/+54PoxSANc= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 h1:CjMzUs78RDDv4ROu3JnJn/Ig1r6ZD7/T2DXLLRpejic= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16/go.mod h1:uVW4OLBqbJXSHJYA9svT9BluSvvwbzLQ2Crf6UPzR3c= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 h1:DIBqIrJ7hv+e4CmIk2z3pyKT+3B6qVMgRsawHiR3qso= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7/go.mod h1:vLm00xmBke75UmpNvOcZQ/Q30ZFjbczeLFqGx5urmGo= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 h1:oHjJHeUy0ImIV0bsrX0X91GkV5nJAyv1l1CC9lnO0TI= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16/go.mod h1:iRSNGgOYmiYwSCXxXaKb9HfOEj40+oTKn8pTxMlYkRM= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 h1:NSbvS17MlI2lurYgXnCOLvCFX38sBW4eiVER7+kkgsU= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16/go.mod h1:SwT8Tmqd4sA6G1qaGdzWCJN99bUmPGHfRwwq3G5Qb+A= +github.com/aws/aws-sdk-go-v2/service/s3 v1.94.0 h1:SWTxh/EcUCDVqi/0s26V6pVUq0BBG7kx0tDTmF/hCgA= +github.com/aws/aws-sdk-go-v2/service/s3 v1.94.0/go.mod h1:79S2BdqCJpScXZA2y+cpZuocWsjGjJINyXnOsf5DTz8= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 h1:HpI7aMmJ+mm1wkSHIA2t5EaFFv5EFYXePW30p1EIrbQ= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.4/go.mod h1:C5RdGMYGlfM0gYq/tifqgn4EbyX99V15P2V3R+VHbQU= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.7 h1:eYnlt6QxnFINKzwxP5/Ucs1vkG7VT3Iezmvfgc2waUw= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.7/go.mod h1:+fWt2UHSb4kS7Pu8y+BMBvJF0EWx+4H0hzNwtDNRTrg= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 h1:AHDr0DaHIAo8c9t1emrzAlVDFp+iMMKnPdYy6XO4MCE= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12/go.mod h1:GQ73XawFFiWxyWXMHWfhiomvP3tXtdNar/fi8z18sx0= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 h1:SciGFVNZ4mHdm7gpD1dgZYnCuVdX1s+lFTg4+4DOy70= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.5/go.mod h1:iW40X4QBmUxdP+fZNOpfmkdMZqsovezbAeO+Ubiv2pk= +github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk= +github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/goccy/go-yaml v1.19.0 h1:EmkZ9RIsX+Uq4DYFowegAuJo8+xdX3T/2dwNPXbxEYE= github.com/goccy/go-yaml v1.19.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= diff --git a/internal/backend/backend.go b/internal/backend/backend.go index 07e90c2..018e86b 100644 --- a/internal/backend/backend.go +++ b/internal/backend/backend.go @@ -3,5 +3,5 @@ package backend import c "github.com/aslamcodes/appstreamfile/internal/config" type BackendSource interface { - GetConfig() c.Config + GetConfig() (*c.Config, error) } diff --git a/internal/backend/local.go b/internal/backend/local.go index c1985be..ce69adf 100644 --- a/internal/backend/local.go +++ b/internal/backend/local.go @@ -31,3 +31,9 @@ func (lb *LocalBackend) GetConfig() (*config.Config, error) { return &configData, nil } + +func NewLocalBackend(location string) (BackendSource, error) { + return &LocalBackend{ + Location: location, + }, nil +} diff --git a/internal/backend/s3.go b/internal/backend/s3.go index e63e100..508de10 100644 --- a/internal/backend/s3.go +++ b/internal/backend/s3.go @@ -1,12 +1,64 @@ package backend import ( - c "github.com/aslamcodes/appstreamfile/internal/config" + "context" + "fmt" + "io" + + "github.com/aslamcodes/appstreamfile/internal/config" + "github.com/goccy/go-yaml" ) type S3Backend struct { + Bucket string + Key string + VersionId string + Client S3Client } -func (s3 *S3Backend) GetConfig() (*c.Config, error) { - return &c.Config{}, nil +func (s3Backend *S3Backend) GetConfig() (*config.Config, error) { + ctx := context.Background() + + if s3Backend.Client == nil { + return nil, fmt.Errorf("client is nil") + } + + out, err := s3Backend.Client.GetObject(ctx, s3Backend.Bucket, s3Backend.Key, s3Backend.VersionId) + + if err != nil { + return nil, fmt.Errorf("failed to fetch object from s3: %w", err) + } + + defer out.Body.Close() + + content, err := io.ReadAll(out.Body) + + if err != nil { + return nil, fmt.Errorf("error reading config: %w", err) + } + + var configData config.Config + + if err := yaml.Unmarshal(content, &configData); err != nil { + return nil, fmt.Errorf("failed to parse config data, config data or formatting is invalid: %w", err) + } + + fmt.Printf("Builder has successfully parsed the config file from backend\n") + + return &configData, nil +} + +func NewS3Backend(bucket, key, versionId, profile string) (BackendSource, error) { + client, err := NewS3Client(profile) + + if err != nil { + return nil, fmt.Errorf("not able to create s3 client: %w", err) + } + + return &S3Backend{ + Bucket: bucket, + Key: key, + VersionId: versionId, + Client: client, + }, nil } diff --git a/internal/backend/s3_client.go b/internal/backend/s3_client.go new file mode 100644 index 0000000..1a2bdd5 --- /dev/null +++ b/internal/backend/s3_client.go @@ -0,0 +1,53 @@ +package backend + +import ( + "context" + "fmt" + + s3Config "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/s3" +) + +type S3Client interface { + GetObject(ctx context.Context, bucket string, key string, versionId string) (*s3.GetObjectOutput, error) +} + +type S3BackendClient struct { + s3Client *s3.Client +} + +func (c *S3BackendClient) GetObject(ctx context.Context, bucket string, key string, versionId string) (*s3.GetObjectOutput, error) { + objectInput := &s3.GetObjectInput{ + Bucket: &bucket, + Key: &key, + } + + if versionId != "" { + objectInput.VersionId = &versionId + } + + return c.s3Client.GetObject(ctx, objectInput) +} + +func NewS3Client(profile string) (S3Client, error) { + opts := []func(*s3Config.LoadOptions) error{} + + if profile != "" { + opts = append(opts, s3Config.WithSharedConfigProfile(profile)) + } + + ctx := context.Background() + + cfg, err := s3Config.LoadDefaultConfig(ctx, opts...) + if err != nil { + return nil, fmt.Errorf("failed to load SDK configuration: %w", err) + } + + client := s3.NewFromConfig(cfg) + + backendClient := S3BackendClient{ + s3Client: client, + } + + return &backendClient, nil +} diff --git a/internal/backend/s3_test.go b/internal/backend/s3_test.go new file mode 100644 index 0000000..f42fb39 --- /dev/null +++ b/internal/backend/s3_test.go @@ -0,0 +1,88 @@ +package backend_test + +import ( + "bytes" + "context" + "errors" + "io" + "reflect" + "testing" + + "github.com/aslamcodes/appstreamfile/internal/backend" + "github.com/aslamcodes/appstreamfile/internal/config" + "github.com/aws/aws-sdk-go-v2/service/s3" +) + +type mockS3Client struct { + Output *s3.GetObjectOutput + Err error +} + +func (client *mockS3Client) GetObject(ctx context.Context, bucket string, key string, versionId string) (*s3.GetObjectOutput, error) { + return client.Output, client.Err +} + +func TestS3GetConfig(t *testing.T) { + client := &mockS3Client{} + + content := `platform: "unix" +installers: + - executable: "bash" + installScript: | + echo "Hello World"` + + expected := &config.Config{ + Platform: "unix", + Installers: []config.Installer{ + { + InstallScript: `echo "Hello World"`, + Executable: "bash", + }, + }, + } + + client.Output = &s3.GetObjectOutput{ + Body: io.NopCloser(bytes.NewReader([]byte(content))), + } + + backend := &backend.S3Backend{ + Bucket: "test", + Key: "test", + VersionId: "", + Client: client, + } + + actual, err := backend.GetConfig() + + if err != nil { + t.Errorf("error fetching the config: %v", err) + } + + if !reflect.DeepEqual(actual, expected) { + t.Errorf("expected %v+,\n actual %v+", expected, actual) + } + +} + +func TestGetConfigFail(t *testing.T) { + expectedErr := errors.New("boom") + + backend := &backend.S3Backend{ + Bucket: "test", + Key: "test", + VersionId: "", + Client: &mockS3Client{ + Err: expectedErr, + }, + } + + _, err := backend.GetConfig() + + if err == nil { + t.Errorf("expected %v, got nil", expectedErr) + } + if !errors.Is(err, expectedErr) { + t.Errorf("expected %v, got %v", expectedErr, err) + } + +}