// Copyright 2024 Matthew Rich . All rights reserved. package resource import ( "context" "decl/tests/mocks" _ "encoding/json" "fmt" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/image" "github.com/stretchr/testify/assert" "io" _ "net/http" _ "net/http/httptest" "net/url" "os" "strings" "testing" "path/filepath" "decl/internal/data" "decl/internal/folio" "decl/internal/codec" //_ "decl/internal/fan" "strconv" ) type MockConfigValueGetter func(key string) (any, error) func (m MockConfigValueGetter) GetValue(key string) (any, error) { return m(key) } func TestNewContainerImageResource(t *testing.T) { c := NewContainerImage(&mocks.MockContainerClient{}) assert.NotNil(t, c) } func TestContainerImageURI(t *testing.T) { case0URI := URIFromContainerImageName("foo") assert.Equal(t, "container-image:///foo", case0URI) case1URI := URIFromContainerImageName("foo:bar") assert.Equal(t, "container-image:///foo:bar", case1URI) case2URI := URIFromContainerImageName("quuz/foo:bar") assert.Equal(t, "container-image:///quuz/foo:bar", case2URI) case3URI := URIFromContainerImageName("myhost/quuz/foo:bar") assert.Equal(t, "container-image://myhost/quuz/foo:bar", case3URI) } func TestLoadFromContainerImageURI(t *testing.T) { testURI := URIFromContainerImageName("myhost/quuz/foo:bar") newResource, resourceErr := folio.DocumentRegistry.ResourceTypes.New(testURI) assert.Nil(t, resourceErr) assert.NotNil(t, newResource) assert.IsType(t, &ContainerImage{}, newResource) assert.Equal(t, "myhost/quuz/foo:bar", newResource.(*ContainerImage).Name) assert.Equal(t, testURI, newResource.URI()) } func TestContainerImageNameFromURI(t *testing.T) { case0u,_ := url.Parse("container-image:///foo") case0Image := ContainerImageNameFromURI(case0u) assert.Equal(t, "foo", case0Image) case1u,_ := url.Parse("container-image:///foo:bar") case1Image := ContainerImageNameFromURI(case1u) assert.Equal(t, "foo:bar", case1Image) case2u,_ := url.Parse("container-image:///quuz/foo:bar") case2Image := ContainerImageNameFromURI(case2u) assert.Equal(t, "quuz/foo:bar", case2Image) case3u,_ := url.Parse("container-image://myhost/quuz/foo:bar") case3Image := ContainerImageNameFromURI(case3u) assert.Equal(t, "myhost/quuz/foo:bar", case3Image) } func TestReadContainerImage(t *testing.T) { output := io.NopCloser(strings.NewReader("testdata")) ctx := context.Background() decl := ` name: "alpine:latest" state: present ` m := &mocks.MockContainerClient{ InjectImagePull: func(ctx context.Context, refStr string, options image.PullOptions) (io.ReadCloser, error) { return output, nil }, InjectImageRemove: func(ctx context.Context, imageID string, options image.RemoveOptions) ([]image.DeleteResponse, error) { return nil, nil }, InjectImageInspectWithRaw: func(ctx context.Context, imageID string) (types.ImageInspect, []byte, error) { return types.ImageInspect{ ID: "sha256:123456789abc", }, nil, nil }, } c := NewContainerImage(m) assert.NotNil(t, c) e := c.LoadDecl(decl) assert.Equal(t, nil, e) assert.Equal(t, "alpine:latest", c.Name) resourceYaml, readContainerErr := c.Read(ctx) assert.Equal(t, nil, readContainerErr) assert.Greater(t, len(resourceYaml), 0) } func TestCreateContainerImage(t *testing.T) { m := &mocks.MockContainerClient{ InjectImageBuild: func(ctx context.Context, buildContext io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error) { return types.ImageBuildResponse{Body: io.NopCloser(strings.NewReader("image built")) }, nil }, } decl := fmt.Sprintf(` name: "testcontainerimage" image: "alpine" contextref: file://%s `, "") c := NewContainerImage(m) stater := c.StateMachine() e := c.LoadDecl(decl) assert.Nil(t, e) assert.Equal(t, "testcontainerimage", c.Name) assert.Nil(t, stater.Trigger("create")) } func TestCreateContainerImagePush(t *testing.T) { m := &mocks.MockContainerClient{ InjectImageBuild: func(ctx context.Context, buildContext io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error) { return types.ImageBuildResponse{Body: io.NopCloser(strings.NewReader("image built")) }, nil }, InjectImagePush: func(ctx context.Context, image string, options image.PushOptions) (io.ReadCloser, error) { return io.NopCloser(strings.NewReader("foo")), nil }, } decl := fmt.Sprintf(` name: "testcontainerimage" image: "alpine" push: true contextref: file://%s `, "") c := NewContainerImage(m) stater := c.StateMachine() e := c.LoadDecl(decl) assert.Nil(t, e) assert.Equal(t, "testcontainerimage", c.Name) c.UseConfig(MockConfigValueGetter(func(key string) (any, error) { switch key { case "repo_password": return "bar", nil case "repo_username": return "foo", nil case "repo_server": return "", nil } return nil, fmt.Errorf("%w: %s", data.ErrUnknownConfigurationKey, key) })) assert.Nil(t, stater.Trigger("create")) assert.Nil(t, c.Push(context.Background())) assert.True(t, c.PushImage) // assert.Equal(t, "foo", c.Output) } func TestContainerImageContextDocument(t *testing.T) { contextDir, _ := filepath.Abs(TempDir.FilePath("context")) etcDir, _ := filepath.Abs(filepath.Join(contextDir, "etc")) binDir, _ := filepath.Abs(filepath.Join(contextDir, "bin")) assert.Nil(t, os.Mkdir(contextDir, os.ModePerm)) assert.Nil(t, os.Mkdir(etcDir, os.ModePerm)) assert.Nil(t, os.Mkdir(binDir, os.ModePerm)) m := &mocks.MockContainerClient{ InjectImageBuild: func(ctx context.Context, buildContext io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error) { return types.ImageBuildResponse{Body: io.NopCloser(strings.NewReader("image built")) }, nil }, } contextDirUri := fmt.Sprintf("file://%s", contextDir) contextDirDecl := fmt.Sprintf(` type: file attributes: path: %s `, contextDir) decl := fmt.Sprintf(` name: "testcontainerimage" image: "alpine" contextref: %s `, contextDirUri) c := NewContainerImage(m) c.ConverterTypes = TestConverterTypes //stater := c.StateMachine() e := c.LoadDecl(decl) assert.Nil(t, e) c.ContextRef = folio.ResourceReference(contextDirUri) contextFile := folio.NewDeclaration() assert.Nil(t, contextFile.NewResource(&contextDirUri)) assert.Nil(t, contextFile.LoadString(contextDirDecl, codec.FormatYaml)) _, readErr := contextFile.Resource().Read(context.Background()) assert.Nil(t, readErr) c.Resources = data.NewResourceMapper() c.Resources.Set(contextDirUri, contextFile) d, contextErr := c.ContextDocument() assert.Nil(t, contextErr) assert.NotNil(t, d) assert.Greater(t, 3, d.Len()) } func TestContainerError(t *testing.T) { var expected, err, boErr ContainerError expected.Detail.ErrorMessage = "invalid reference format" expected.Error = "invalid reference format" buildOutput := `"{\"stream\":\"Step 1/6 : ARG DIST\"}\r\n{\"stream\":\"\\n\"}\r\n{\"stream\":\"Step 2/6 : FROM golang:${DIST}\"}\r\n{\"stream\":\"\\n\"}\r\n{\"errorDetail\":{\"message\":\"invalid reference format\"},\"error\":\"invalid reference format\"}\r\n"` unqBuildOutput, buildOutputErr := strconv.Unquote(buildOutput) assert.Nil(t, buildOutputErr) dec := codec.NewJSONStringDecoder(strings.Split(unqBuildOutput, "\r\n")[4]) assert.Nil(t, dec.Decode(&boErr)) assert.Equal(t, expected, boErr) msg := `"{\"errorDetail\":{\"message\":\"invalid reference format\"},\"error\":\"invalid reference format\"}"` unquotedmsg := `{"errorDetail":{"message":"invalid reference format"},"error":"invalid reference format"}` c,_ := strconv.Unquote(msg) assert.Equal(t, unquotedmsg, c) decoder := codec.NewJSONStringDecoder(c) assert.Nil(t, decoder.Decode(&err)) assert.Equal(t, expected, err) } func TestContainerLogUnmarshal(t *testing.T) { /* m := &mocks.MockContainerClient{ InjectImageBuild: func(ctx context.Context, buildContext io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error) { return types.ImageBuildResponse{Body: io.NopCloser(strings.NewReader("image built")) }, nil }, } c := NewContainerImage(m) assert.NotNil(t, c) */ buildOutput := `"{\"stream\":\"Step 1/6 : ARG DIST\"}\r\n{\"stream\":\"\\n\"}\r\n{\"stream\":\"Step 2/6 : FROM golang:${DIST}\"}\r\n{\"stream\":\"\\n\"}\r\n{\"errorDetail\":{\"message\":\"invalid reference format\"},\"error\":\"invalid reference format\"}\r\n"` unqBuildOutput, buildOutputErr := strconv.Unquote(buildOutput) assert.Nil(t, buildOutputErr) for _, v := range strings.Split(unqBuildOutput, "\r\n") { var clog ContainerLog outputDecoder := codec.NewJSONStringDecoder(v) err := outputDecoder.Decode(&clog) if err == io.EOF { break } if clog.ContainerLogStream != nil { assert.Greater(t, len(clog.ContainerLogStream.Stream), 0) } } //assert.Equal(t, "Step 1/6 : ARG DIST", clog[0].ContainerLogStream.Stream) } func TestContainerImageRegistryAuthConfig(t *testing.T) { m := &mocks.MockContainerClient{ InjectImageBuild: func(ctx context.Context, buildContext io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error) { return types.ImageBuildResponse{Body: io.NopCloser(strings.NewReader("image built")) }, nil }, } c := NewContainerImage(m) assert.NotNil(t, c) c.UseConfig(MockConfigValueGetter(func(key string) (any, error) { switch key { case "repo_password": return "bar", nil case "repo_username": return "foo", nil case "repo_server": return "", nil } return nil, fmt.Errorf("%w: %s", data.ErrUnknownConfigurationKey, key) })) authConfig, err := c.RegistryAuthConfig() assert.NotNil(t, authConfig) assert.Nil(t, err) assert.Equal(t, "bar", authConfig.Password) assert.Equal(t, "foo", authConfig.Username) } func TestContainerImageRegistryAuth(t *testing.T) { m := &mocks.MockContainerClient{ InjectImageBuild: func(ctx context.Context, buildContext io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error) { return types.ImageBuildResponse{Body: io.NopCloser(strings.NewReader("image built")) }, nil }, } c := NewContainerImage(m) assert.NotNil(t, c) c.UseConfig(MockConfigValueGetter(func(key string) (any, error) { switch key { case "repo_password": return "bar", nil case "repo_username": return "foo", nil case "repo_server": return "", nil } return nil, fmt.Errorf("%w: %s", data.ErrUnknownConfigurationKey, key) })) auth, err := c.RegistryAuth() assert.NotNil(t, auth) assert.Nil(t, err) assert.Greater(t, len(auth), 1) }