diff --git a/agent.codefly.yaml b/agent.codefly.yaml index 809cce9..2119ebf 100644 --- a/agent.codefly.yaml +++ b/agent.codefly.yaml @@ -5,3 +5,6 @@ version: 0.0.18 release: owner: workflow workflow: releaser.yml +conformance: + mode: attach-existing-source + fixture: testdata/workspace diff --git a/builder.go b/builder.go index f26b55c..de197a9 100644 --- a/builder.go +++ b/builder.go @@ -124,6 +124,17 @@ func (s *Builder) Init(ctx context.Context, req *builderv0.InitRequest) (*builde return s.Builder.InitResponse() } +// Temporal runs an upstream image and owns no generated source files. Both sync +// modes therefore leave the loaded service unchanged, with no drift to report. +func (s *Builder) Sync(context.Context, *builderv0.SyncRequest) (*builderv0.SyncResponse, error) { + return s.Builder.SyncResponse() +} + +func (s *Builder) Build(context.Context, *builderv0.BuildRequest) (*builderv0.BuildResponse, error) { + s.Builder.WithDockerImages(image) + return s.Builder.BuildResponse() +} + func (s *Builder) Audit(ctx context.Context, req *builderv0.AuditRequest) (*builderv0.AuditResponse, error) { defer s.Wool.Catch() ctx = s.Wool.Inject(ctx) diff --git a/conformance_fixture_test.go b/conformance_fixture_test.go new file mode 100644 index 0000000..fb2c8fa --- /dev/null +++ b/conformance_fixture_test.go @@ -0,0 +1,33 @@ +package main + +import ( + "testing" + + "github.com/codefly-dev/core/resources" +) + +func TestConformanceFixtureLoadsCandidateWithPostgresDependency(t *testing.T) { + workspace, err := resources.LoadWorkspaceFromDir(t.Context(), "testdata/workspace") + if err != nil { + t.Fatal(err) + } + services, err := workspace.LoadServices(t.Context()) + if err != nil { + t.Fatal(err) + } + var candidate, database *resources.Service + for _, service := range services { + switch service.Name { + case "temporal": + candidate = service + case "postgres": + database = service + } + } + if candidate == nil || database == nil || candidate.Agent.Version != "latest" || database.Agent.Version != "0.0.134" { + t.Fatal("fixture must qualify the candidate against its declared published database agent") + } + if len(candidate.ServiceDependencies) != 1 || candidate.ServiceDependencies[0].Name != database.Name { + t.Fatal("Temporal conformance must include its actual persistence dependency") + } +} diff --git a/main.go b/main.go index 58ad42c..e374478 100644 --- a/main.go +++ b/main.go @@ -58,9 +58,24 @@ func (s *Service) GetAgentInformation(ctx context.Context, _ *agentv0.AgentInfor Backends: runnersbase.BackendSupport{ Local: func() bool { return languages.HasGoRuntime(nil) }, Nix: false, - Docker: true, + Docker: false, }, Toolchains: []agentv0.Toolchain_Type{agentv0.Toolchain_GO}, + Validation: &agentv0.ValidationCapabilities{ + Test: &agentv0.TestValidationCapability{ + Supported: true, + Scopes: []agentv0.ValidationScope{agentv0.ValidationScope_VALIDATION_SCOPE_WORKSPACE}, + Suites: []*agentv0.TestSuiteCapability{{ + Name: "readiness", + DefaultSuite: true, + DependencyMode: agentv0.TestDependencyMode_TEST_DEPENDENCY_MODE_START_STACK, + }}, + }, + Sync: &agentv0.ValidationOperationCapability{Supported: true, Scopes: []agentv0.ValidationScope{agentv0.ValidationScope_VALIDATION_SCOPE_WORKSPACE}}, + ArtifactBuild: &agentv0.ValidationOperationCapability{Supported: true, Scopes: []agentv0.ValidationScope{agentv0.ValidationScope_VALIDATION_SCOPE_WORKSPACE}}, + Audit: &agentv0.ValidationOperationCapability{Supported: true}, + Sbom: &agentv0.ValidationOperationCapability{Supported: true}, + }, Config: []*agentv0.ConfigurationValueDetail{ { Name: "connection", Description: "Temporal connection details", diff --git a/main_test.go b/main_test.go index e5ba8f4..46952d0 100644 --- a/main_test.go +++ b/main_test.go @@ -223,6 +223,11 @@ func bootTemporal(ctx context.Context, t *testing.T, pgConn string) string { _, err = runtime.Start(ctx, &runtimev0.StartRequest{}) require.NoError(t, err) t.Logf("Temporal running on port %d", runtime.grpcPort) + readiness, err := runtime.Test(ctx, &runtimev0.TestRequest{Suite: "readiness"}) + require.NoError(t, err) + require.Equal(t, runtimev0.TestStatus_SUCCESS, readiness.GetStatus().GetState()) + require.EqualValues(t, 1, readiness.GetTestsRun()) + require.EqualValues(t, 1, readiness.GetTestsPassed()) // Extract gRPC connection temporalConf, err := resources.ExtractConfiguration(temporalInitResp.RuntimeConfigurations, resources.NewRuntimeContextNative()) diff --git a/managed_image_test.go b/managed_image_test.go new file mode 100644 index 0000000..260f52d --- /dev/null +++ b/managed_image_test.go @@ -0,0 +1,74 @@ +package main + +import ( + "io/fs" + "os" + "testing" + + basev0 "github.com/codefly-dev/core/generated/go/codefly/base/v0" + agentv0 "github.com/codefly-dev/core/generated/go/codefly/services/agent/v0" + builderv0 "github.com/codefly-dev/core/generated/go/codefly/services/builder/v0" + "github.com/stretchr/testify/require" +) + +func TestManagedImageSyncAndBuild(t *testing.T) { + root := t.TempDir() + require.NoError(t, os.CopyFS(root, os.DirFS("testdata/workspace"))) + builder := NewBuilder() + loaded, err := builder.Load(t.Context(), &builderv0.LoadRequest{ + Identity: &basev0.ServiceIdentity{ + Name: "temporal", Version: "0.0.0", Module: "temporal-test", + Workspace: "temporal-test", WorkspacePath: root, + RelativeToWorkspace: "services/temporal", + }, + }) + require.NoError(t, err) + require.Equal(t, builderv0.LoadStatus_READY, loaded.GetState().GetState()) + before := snapshotFiles(t, root) + for _, dryRun := range []bool{true, false} { + response, err := builder.Sync(t.Context(), &builderv0.SyncRequest{DryRun: dryRun}) + require.NoError(t, err) + require.Equal(t, builderv0.SyncStatus_SUCCESS, response.GetState().GetState()) + require.Empty(t, response.GetChangedFiles()) + require.Equal(t, before, snapshotFiles(t, root)) + } + response, err := builder.Build(t.Context(), &builderv0.BuildRequest{}) + require.NoError(t, err) + require.Equal(t, builderv0.BuildStatus_SUCCESS, response.GetState().GetState()) + require.Equal(t, []string{image.FullName()}, response.GetResult().GetDockerBuildResult().GetImages()) + require.NotEmpty(t, image.Digest) + require.Equal(t, before, snapshotFiles(t, root)) + + info, err := builder.GetAgentInformation(t.Context(), &agentv0.AgentInformationRequest{}) + require.NoError(t, err) + require.True(t, info.GetValidation().GetSync().GetSupported()) + require.True(t, info.GetValidation().GetArtifactBuild().GetSupported()) + // There is no native source to lint or compile in this managed-image service. + require.False(t, info.GetValidation().GetLint().GetSupported()) + require.False(t, info.GetValidation().GetCompile().GetSupported()) +} + +func TestManagedImageOperationsRequireLoadedService(t *testing.T) { + builder := NewBuilder() + sync, err := builder.Sync(t.Context(), &builderv0.SyncRequest{DryRun: true}) + require.NoError(t, err) + require.Equal(t, builderv0.SyncStatus_ERROR, sync.GetState().GetState()) + build, err := builder.Build(t.Context(), &builderv0.BuildRequest{}) + require.NoError(t, err) + require.Equal(t, builderv0.BuildStatus_ERROR, build.GetState().GetState()) +} + +func snapshotFiles(t *testing.T, root string) map[string]string { + t.Helper() + files := map[string]string{} + filesystem := os.DirFS(root) + require.NoError(t, fs.WalkDir(filesystem, ".", func(path string, entry fs.DirEntry, err error) error { + if err != nil || entry.IsDir() { + return err + } + contents, err := fs.ReadFile(filesystem, path) + files[path] = string(contents) + return err + })) + return files +} diff --git a/readiness_test.go b/readiness_test.go new file mode 100644 index 0000000..1fa3a9c --- /dev/null +++ b/readiness_test.go @@ -0,0 +1,42 @@ +package main + +import ( + "testing" + + agentv0 "github.com/codefly-dev/core/generated/go/codefly/services/agent/v0" + runtimev0 "github.com/codefly-dev/core/generated/go/codefly/services/runtime/v0" + "github.com/stretchr/testify/require" +) + +func TestReadinessAdvertisesItsLiveStack(t *testing.T) { + info, err := NewService().GetAgentInformation(t.Context(), &agentv0.AgentInformationRequest{}) + require.NoError(t, err) + tests := info.GetValidation().GetTest() + require.True(t, tests.GetSupported()) + require.Len(t, tests.GetSuites(), 1) + suite := tests.GetSuites()[0] + require.Equal(t, "readiness", suite.GetName()) + require.True(t, suite.GetDefaultSuite()) + require.Equal(t, agentv0.TestDependencyMode_TEST_DEPENDENCY_MODE_START_STACK, suite.GetDependencyMode()) +} + +func TestReadinessCannotPassWithoutRunningStack(t *testing.T) { + response, err := NewRuntime().Test(t.Context(), &runtimev0.TestRequest{}) + require.NoError(t, err) + require.Equal(t, runtimev0.TestStatus_ERROR, response.GetStatus().GetState()) + require.Contains(t, response.GetStatus().GetMessage(), "started service stack") +} + +func TestReadinessRejectsUnsupportedRequests(t *testing.T) { + for _, request := range []*runtimev0.TestRequest{ + {Suite: "missing"}, {Target: "./..."}, {Filters: []string{"test"}}, + {Timeout: "invalid"}, {Timeout: "0s"}, {Race: true}, {Coverage: true}, + {ExtraArgs: []string{"--flag"}}, {Formula: &runtimev0.TestFormula{}}, + {Selection: &runtimev0.TestSelection{Scope: &runtimev0.TestSelection_Suite{Suite: &runtimev0.TestSuiteSelection{Name: "missing"}}}}, + } { + response, err := NewRuntime().Test(t.Context(), request) + require.NoError(t, err) + require.Equal(t, runtimev0.TestStatus_ERROR, response.GetStatus().GetState()) + require.NotContains(t, response.GetStatus().GetMessage(), "started service stack") + } +} diff --git a/runtime.go b/runtime.go index 3ed238a..cbb3f39 100644 --- a/runtime.go +++ b/runtime.go @@ -103,10 +103,10 @@ func (s *Runtime) CreateConnectionsConfiguration(runtimeContext *basev0.RuntimeC } } -// resolvePostgresFromDependencies extracts Postgres connection details from -// codefly dependency configurations (postgres service). +// The embedded server executes in the agent process, not in a service container. +// Select the host projection before inspecting the database's declarations. func (s *Runtime) resolvePostgresFromDependencies(confs []*basev0.Configuration) error { - for _, conf := range confs { + for _, conf := range resources.FilterConfigurations(confs, resources.NewRuntimeContextNative()) { for _, info := range conf.Infos { if info.Name == "postgres" { for _, cv := range info.ConfigurationValues { @@ -186,8 +186,7 @@ func (s *Runtime) Init(ctx context.Context, req *runtimev0.InitRequest) (*runtim s.Infof("Temporal gRPC will run on localhost:%d", s.grpcPort) s.Infof("Temporal HTTP will run on localhost:%d", s.httpPort) - // Resolve Postgres connection from dependency configurations - // Filter for the appropriate runtime context (like go-grpc does) + // Resolve the host-side Postgres configuration for the embedded server. if req.DependenciesConfigurations == nil || len(req.DependenciesConfigurations) == 0 { return s.Runtime.InitErrorf(fmt.Errorf("no dependencies configurations"), "temporal requires postgres dependency") } @@ -467,7 +466,37 @@ func (s *Runtime) Destroy(ctx context.Context, req *runtimev0.DestroyRequest) (* } func (s *Runtime) Test(ctx context.Context, req *runtimev0.TestRequest) (*runtimev0.TestResponse, error) { - return s.Runtime.TestResponse() + if req.GetSuite() != "" && req.GetSuite() != "readiness" { + return s.Runtime.TestError(fmt.Errorf("unknown Temporal test suite %q", req.GetSuite())) + } + if selection := req.GetSelection(); selection != nil && selection.GetSuite().GetName() != "readiness" { + return s.Runtime.TestError(fmt.Errorf("Temporal tests support only the readiness suite")) + } + if req.GetTarget() != "" || len(req.GetFilters()) != 0 || len(req.GetExtraArgs()) != 0 || req.GetFormula() != nil || req.GetRace() || req.GetCoverage() { + return s.Runtime.TestError(fmt.Errorf("Temporal readiness does not support source-test selectors or instrumentation")) + } + timeout := 30 * time.Second + if req.GetTimeout() != "" { + var err error + timeout, err = time.ParseDuration(req.GetTimeout()) + if err != nil || timeout <= 0 { + return s.Runtime.TestError(fmt.Errorf("invalid readiness timeout %q", req.GetTimeout())) + } + } + if s.temporalServer == nil || s.grpcPort == 0 { + return s.Runtime.TestError(fmt.Errorf("Temporal readiness requires the started service stack")) + } + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + conn, err := grpc.NewClient(fmt.Sprintf("localhost:%d", s.grpcPort), grpc.WithTransportCredentials(insecure.NewCredentials())) + if err == nil { + defer conn.Close() + _, err = workflowservice.NewWorkflowServiceClient(conn).GetSystemInfo(ctx, &workflowservice.GetSystemInfoRequest{}) + } + if err != nil { + return s.Runtime.TestResponseWithResults(1, 0, 1, 0, 0, []string{err.Error()}, err) + } + return s.Runtime.TestResponseWithResults(1, 1, 0, 0, 0, nil, nil) } // parsePostgresConnectionString extracts host, port, user, password from a diff --git a/runtime_configuration_test.go b/runtime_configuration_test.go new file mode 100644 index 0000000..9148d2b --- /dev/null +++ b/runtime_configuration_test.go @@ -0,0 +1,36 @@ +package main + +import ( + "testing" + + basev0 "github.com/codefly-dev/core/generated/go/codefly/base/v0" + agentv0 "github.com/codefly-dev/core/generated/go/codefly/services/agent/v0" + "github.com/codefly-dev/core/resources" + "github.com/stretchr/testify/require" +) + +func TestEmbeddedRuntimeSelectsNativeDatabaseConfiguration(t *testing.T) { + configuration := func(runtime *basev0.RuntimeContext, connection string) *basev0.Configuration { + return &basev0.Configuration{RuntimeContext: runtime, Infos: []*basev0.ConfigurationInformation{{ + Name: "postgres", ConfigurationValues: []*basev0.ConfigurationValue{{Key: "owner-connection", Value: connection, Secret: true}}, + }}} + } + container := configuration(resources.NewRuntimeContextContainer(), "postgresql://container:wrong@host.docker.internal:6432/postgres") + native := configuration(resources.NewRuntimeContextNative(), "postgresql://native:correct@localhost:5433/postgres") + for _, configs := range [][]*basev0.Configuration{{container, native}, {native, container}} { + runtime := NewRuntime() + require.NoError(t, runtime.resolvePostgresFromDependencies(configs)) + require.Equal(t, "localhost", runtime.postgresHost) + require.Equal(t, 5433, runtime.postgresPort) + require.Equal(t, "native", runtime.postgresUser) + require.Equal(t, "correct", runtime.postgresPassword) + } + for _, configs := range [][]*basev0.Configuration{nil, {container}, {nil, container}} { + require.Error(t, NewRuntime().resolvePostgresFromDependencies(configs), "a container-only projection must not become a host connection") + } + info, err := NewService().GetAgentInformation(t.Context(), &agentv0.AgentInformationRequest{}) + require.NoError(t, err) + for _, backend := range info.GetSupportedBackends() { + require.NotEqual(t, agentv0.Backend_DOCKER, backend.GetType(), "deployment's managed image is not an implemented runtime backend") + } +} diff --git a/testdata/workspace/services/postgres/service.codefly.yaml b/testdata/workspace/services/postgres/service.codefly.yaml index 998808c..8c789cb 100644 --- a/testdata/workspace/services/postgres/service.codefly.yaml +++ b/testdata/workspace/services/postgres/service.codefly.yaml @@ -3,7 +3,7 @@ version: 0.0.0 agent: kind: codefly:service name: postgres - version: 0.0.133 + version: 0.0.134 publisher: codefly.dev endpoints: - name: tcp diff --git a/testdata/workspace/services/temporal/service.codefly.yaml b/testdata/workspace/services/temporal/service.codefly.yaml index 09299b1..8926371 100644 --- a/testdata/workspace/services/temporal/service.codefly.yaml +++ b/testdata/workspace/services/temporal/service.codefly.yaml @@ -3,7 +3,7 @@ version: 0.0.0 agent: kind: codefly:service name: temporal - version: 0.0.17 + version: latest publisher: codefly.dev service-dependencies: - name: postgres