Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions lib/compose/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,31 @@ services:
tls: true
```

By default, compose names resources from the compose name and service key:

```text
instance: <compose name>-<service>
ingress: <compose name>-<service>-<index>
Comment thread
sjmiller609 marked this conversation as resolved.
```

Set `name` on a service or ingress rule when a stable external name is required:

```yaml
version: 1
name: hypeship

services:
otelcol:
name: hypeship-otelcol-${env:DEPLOY_ENV}
image: otel/opentelemetry-collector-contrib:0.108.0
ingress:
- name: hypeship-otelcol-${env:DEPLOY_ENV}-otlp
hostname: hypeship-otelcol-${env:DEPLOY_ENV}.dev-yul-hypeman-0.kernel.sh
host_port: 443
target_port: 3000
tls: true
```

### Commands

Preview the changes:
Expand Down
186 changes: 185 additions & 1 deletion lib/compose/compose_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ func TestLoadComposeSpecInterpolatesFilesAndEnv(t *testing.T) {
t.Setenv("COMPOSE_NAME", "hypeship-otel")
t.Setenv("OTEL_IMAGE", "otel/opentelemetry-collector-contrib:0.108.0")
t.Setenv("OTELCOL_ENV_NAME", "OTELCOL_CONFIG")
t.Setenv("DEPLOY_ENV", "staging")
t.Setenv("OTEL_COLLECTOR_VM_HOSTNAME", "otel.example.com")
t.Setenv("OTEL_COLLECTOR_VM_TOKEN", "collector-token")
t.Setenv("SIGNOZ_ACCESS_TOKEN", "secret-token")
Expand All @@ -33,13 +34,15 @@ version: 1
name: ${env:COMPOSE_NAME}
services:
otelcol:
name: hypeship-otelcol-${env:DEPLOY_ENV}
image: ${env:OTEL_IMAGE}
cmd: ["--config=env:${env:OTELCOL_ENV_NAME}"]
env:
OTELCOL_CONFIG: ${file:otelcol.yaml}
SIGNOZ_ACCESS_TOKEN: ${env:SIGNOZ_ACCESS_TOKEN}
ingress:
- hostname: ${env:OTEL_COLLECTOR_VM_HOSTNAME}
- name: hypeship-otelcol-${env:DEPLOY_ENV}-otlp
hostname: ${env:OTEL_COLLECTOR_VM_HOSTNAME}
target_port: 4318
`), 0644))

Expand All @@ -48,11 +51,13 @@ services:

service := spec.Services["otelcol"]
assert.Equal(t, "hypeship-otel", spec.Name)
assert.Equal(t, "hypeship-otelcol-staging", service.Name)
assert.Equal(t, "otel/opentelemetry-collector-contrib:0.108.0", service.Image)
assert.Equal(t, []string{"--config=env:OTELCOL_CONFIG"}, service.Cmd)
assert.Equal(t, "endpoint: https://otel.example.com\ntoken: collector-token\n", service.Env["OTELCOL_CONFIG"])
assert.Equal(t, "secret-token", service.Env["SIGNOZ_ACCESS_TOKEN"])
require.Len(t, service.Ingress, 1)
assert.Equal(t, "hypeship-otelcol-staging-otlp", service.Ingress[0].Name)
assert.Equal(t, "otel.example.com", service.Ingress[0].Hostname)
}

Expand Down Expand Up @@ -149,6 +154,45 @@ func TestDesiredResourcesUseDeterministicNamesAndTags(t *testing.T) {
assert.Equal(t, int64(4318), ingresses[0].Input.Rules[0].Target.Port)
}

func TestDesiredResourcesUseExplicitResourceNames(t *testing.T) {
runner := Runner{
spec: composeSpec{
Version: 1,
Name: "hypeship",
Services: map[string]composeServiceSpec{
"otelcol": {
Name: "hypeship-otelcol-staging",
Image: "otel/opentelemetry-collector-contrib:0.108.0",
Ingress: []composeIngressRuleSpec{
{
Name: "hypeship-otelcol-staging-otlp",
Hostname: "hypeship-otelcol-staging.dev-yul-hypeman-0.kernel.sh",
HostPort: 443,
TargetPort: 3000,
TLS: true,
},
},
},
},
},
}

_, instances, ingresses, _, err := runner.desiredResources()
require.NoError(t, err)

require.Len(t, instances, 1)
assert.Equal(t, "hypeship-otelcol-staging", instances[0].Name)
assert.Equal(t, "otelcol", instances[0].Service)
assert.Equal(t, "hypeship-otelcol-staging", instances[0].Input.Name)
assert.Equal(t, "otelcol", instances[0].Input.Tags[composeTagService])

require.Len(t, ingresses, 1)
assert.Equal(t, "hypeship-otelcol-staging-otlp", ingresses[0].Name)
assert.Equal(t, "hypeship-otelcol-staging", ingresses[0].Input.Rules[0].Target.Instance)
assert.Equal(t, int64(3000), ingresses[0].Input.Rules[0].Target.Port)
assert.Equal(t, "otelcol", ingresses[0].Input.Tags[composeTagService])
}

func TestValidateComposeSpecRejectsInvalidNames(t *testing.T) {
err := validateComposeSpec(&composeSpec{
Version: 1,
Expand All @@ -161,6 +205,40 @@ func TestValidateComposeSpecRejectsInvalidNames(t *testing.T) {
require.EqualError(t, err, "compose name must contain only lowercase letters, digits, and dashes")
}

func TestValidateComposeSpecRejectsInvalidExplicitResourceNames(t *testing.T) {
err := validateComposeSpec(&composeSpec{
Version: 1,
Name: "worker-stack",
Services: map[string]composeServiceSpec{
"worker": {
Name: "BadName",
Image: "alpine:latest",
},
},
})

require.EqualError(t, err, `service "worker" name must contain only lowercase letters, digits, and dashes`)
}

func TestValidateComposeSpecRejectsDuplicateExplicitResourceNames(t *testing.T) {
err := validateComposeSpec(&composeSpec{
Version: 1,
Name: "worker-stack",
Services: map[string]composeServiceSpec{
"api": {
Name: "shared-worker",
Image: "alpine:latest",
},
"worker": {
Name: "shared-worker",
Image: "alpine:latest",
},
},
})

require.ErrorContains(t, err, `produces duplicate instance name "shared-worker"`)
}

func TestValidateComposeSpecRejectsImageAndDockerfile(t *testing.T) {
err := validateComposeSpec(&composeSpec{
Version: 1,
Expand Down Expand Up @@ -292,3 +370,109 @@ func TestConflictBlockers(t *testing.T) {

require.Equal(t, []string{" instance app-api: name exists without compose ownership"}, blockers)
}

func TestPlanInstanceActionReplacesOwnedServiceWhenNameChanges(t *testing.T) {
desired := desiredInstance{
Name: "hypeship-otelcol-production",
Service: "otelcol",
Hash: "new-hash",
Input: hypeman.InstanceNewParams{
Name: "hypeship-otelcol-production",
},
}
owned := []hypeman.Instance{{
ID: "old-instance-id",
Name: "hypeship-otelcol-staging",
Tags: composeTags("hypeship", "otelcol", composeResourceInstance, "old-hash"),
}}

action := planInstanceAction(desired, owned, nil)

assert.Equal(t, "replace", action.Action)
assert.Equal(t, "instance", action.Type)
assert.Equal(t, "hypeship-otelcol-production", action.Name)
assert.Equal(t, "name changed from hypeship-otelcol-staging", action.Reason)
assert.Equal(t, "old-instance-id", action.instanceID)
}

func TestPlanIngressActionReplacesOwnedServiceWhenNameChanges(t *testing.T) {
desired := desiredIngress{
Name: "hypeship-otelcol-production-otlp",
Service: "otelcol",
Hash: "new-hash",
Input: hypeman.IngressNewParams{
Name: "hypeship-otelcol-production-otlp",
},
}
owned := []hypeman.Ingress{{
ID: "old-ingress-id",
Name: "hypeship-otelcol-staging-otlp",
Tags: composeTags("hypeship", "otelcol", composeResourceIngress, "old-hash"),
}}

action := planIngressAction(desired, owned, nil, map[string]struct{}{
"hypeship-otelcol-production-otlp": {},
})

assert.Equal(t, "replace", action.Action)
assert.Equal(t, "ingress", action.Type)
assert.Equal(t, "hypeship-otelcol-production-otlp", action.Name)
assert.Equal(t, "name changed from hypeship-otelcol-staging-otlp", action.Reason)
assert.Equal(t, "old-ingress-id", action.ingressID)
}

func TestPlanIngressActionDoesNotReplaceIngressStillDesiredByAnotherRule(t *testing.T) {
desired := desiredIngress{
Name: "app-api-grpc",
Service: "api",
Hash: "grpc-hash",
Input: hypeman.IngressNewParams{
Name: "app-api-grpc",
},
}
owned := []hypeman.Ingress{{
ID: "http-ingress-id",
Name: "app-api-http",
Tags: composeTags("app", "api", composeResourceIngress, "http-hash"),
}}

action := planIngressAction(desired, owned, nil, map[string]struct{}{
"app-api-http": {},
"app-api-grpc": {},
})

assert.Equal(t, "create", action.Action)
assert.Equal(t, "missing", action.Reason)
assert.Empty(t, action.ingressID)
}

func TestPlanIngressActionConflictsWhenRenameCandidateIsAmbiguous(t *testing.T) {
desired := desiredIngress{
Name: "app-api-public",
Service: "api",
Hash: "public-hash",
Input: hypeman.IngressNewParams{
Name: "app-api-public",
},
}
owned := []hypeman.Ingress{
{
ID: "old-http-id",
Name: "app-api-http",
Tags: composeTags("app", "api", composeResourceIngress, "http-hash"),
},
{
ID: "old-grpc-id",
Name: "app-api-grpc",
Tags: composeTags("app", "api", composeResourceIngress, "grpc-hash"),
},
}

action := planIngressAction(desired, owned, nil, map[string]struct{}{
"app-api-public": {},
})

assert.Equal(t, "conflict", action.Action)
assert.Equal(t, "multiple owned ingresses for service have changed names", action.Reason)
assert.Empty(t, action.ingressID)
}
14 changes: 10 additions & 4 deletions lib/compose/desired.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ func (r *Runner) desiredResources() ([]desiredBuild, []desiredInstance, []desire
builds = append(builds, build)
service.Image = build.Image
}
instanceName := composeInstanceName(r.spec.Name, serviceName)
instanceName := composeInstanceName(r.spec.Name, serviceName, service)
instanceInput := buildComposeInstanceInput(instanceName, service)
instanceHash, err := shortHash(instanceInput)
if err != nil {
Expand All @@ -70,7 +70,7 @@ func (r *Runner) desiredResources() ([]desiredBuild, []desiredInstance, []desire
})

for i, ingressSpec := range service.Ingress {
ingressName := composeIngressName(r.spec.Name, serviceName, i)
ingressName := composeIngressName(r.spec.Name, serviceName, i, ingressSpec)
ingressInput := buildComposeIngressInput(instanceName, ingressName, ingressSpec)
ingressHash, err := shortHash(ingressInput)
if err != nil {
Expand Down Expand Up @@ -253,11 +253,17 @@ func buildComposeIngressInput(instanceName, ingressName string, spec composeIngr
}
}

func composeInstanceName(composeName, serviceName string) string {
func composeInstanceName(composeName, serviceName string, service composeServiceSpec) string {
if service.Name != "" {
return service.Name
}
return composeName + "-" + serviceName
}

func composeIngressName(composeName, serviceName string, index int) string {
func composeIngressName(composeName, serviceName string, index int, ingress composeIngressRuleSpec) string {
if ingress.Name != "" {
return ingress.Name
}
return fmt.Sprintf("%s-%s-%d", composeName, serviceName, index)
}

Expand Down
Loading
Loading