Skip to content

Commit

Permalink
feat: Add label_file support to service
Browse files Browse the repository at this point in the history
Signed-off-by: Suleiman Dibirov <[email protected]>
  • Loading branch information
idsulik committed Nov 23, 2024
1 parent 58f8cad commit ee29602
Show file tree
Hide file tree
Showing 13 changed files with 279 additions and 3 deletions.
2 changes: 2 additions & 0 deletions loader/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,8 @@ func modelToProject(dict map[string]interface{}, opts *Options, configDetails ty
return nil, err
}
}

project, err = project.WithServicesLabelsResolved(opts.discardEnvFiles)

Check failure on line 641 in loader/loader.go

View workflow job for this annotation

GitHub Actions / test (1.22, macos-latest)

ineffectual assignment to err (ineffassign)

Check failure on line 641 in loader/loader.go

View workflow job for this annotation

GitHub Actions / test (1.21, macos-latest)

ineffectual assignment to err (ineffassign)
return project, nil
}

Expand Down
25 changes: 25 additions & 0 deletions loader/loader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2315,6 +2315,31 @@ func TestLoadServiceWithEnvFile(t *testing.T) {
assert.Equal(t, "YES", *service.Environment["HALLO"])
}

func TestLoadServiceWithLabelFile(t *testing.T) {
file, err := os.CreateTemp("", "test-compose-go")
assert.NilError(t, err)
defer os.Remove(file.Name())

_, err = file.Write([]byte("MY_LABEL=MY_VALUE"))
assert.NilError(t, err)

p := &types.Project{
Services: types.Services{
"test": {
Name: "test",
LabelFiles: []types.LabelFile{
{Path: file.Name(), Required: true},
},
},
},
}
p, err = p.WithServicesLabelsResolved(false)
assert.NilError(t, err)
service, err := p.GetService("test")
assert.NilError(t, err)
assert.Equal(t, "MY_VALUE", service.Labels["MY_LABEL"])
}

func TestLoadNoSSHInBuildConfig(t *testing.T) {
actual, err := loadYAML(`
name: load-no-ssh-in-build-config
Expand Down
1 change: 1 addition & 0 deletions override/merge.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ func init() {
mergeSpecials["services.*.dns_search"] = mergeToSequence
mergeSpecials["services.*.entrypoint"] = override
mergeSpecials["services.*.env_file"] = mergeToSequence
mergeSpecials["services.*.label_file"] = mergeToSequence
mergeSpecials["services.*.environment"] = mergeToSequence
mergeSpecials["services.*.extra_hosts"] = mergeExtraHosts
mergeSpecials["services.*.healthcheck.test"] = override
Expand Down
14 changes: 14 additions & 0 deletions override/uncity.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ func init() {
unique["services.*.env_file"] = envFileIndexer
unique["services.*.expose"] = exposeIndexer
unique["services.*.labels"] = keyValueIndexer
unique["services.*.label_file"] = labelFileIndexer
unique["services.*.links"] = keyValueIndexer
unique["services.*.networks.*.aliases"] = keyValueIndexer
unique["services.*.networks.*.link_local_ips"] = keyValueIndexer
Expand Down Expand Up @@ -227,3 +228,16 @@ func envFileIndexer(y any, p tree.Path) (string, error) {
}
return "", nil
}

func labelFileIndexer(y any, p tree.Path) (string, error) {
switch value := y.(type) {
case string:
return value, nil
case map[string]any:
if pathValue, ok := value["path"]; ok {
return pathValue.(string), nil
}
return "", fmt.Errorf("label path attribute %s is missing", p)
}
return "", nil
}
1 change: 1 addition & 0 deletions paths/resolve.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ func ResolveRelativePaths(project map[string]any, base string, remotes []RemoteR
"services.*.build.context": r.absContextPath,
"services.*.build.additional_contexts.*": r.absContextPath,
"services.*.env_file.*.path": r.absPath,
"services.*.label_file.*.path": r.absPath,
"services.*.extends.file": r.absExtendsPath,
"services.*.develop.watch.*.path": r.absSymbolicLink,
"services.*.volumes.*": r.absVolumeMount,
Expand Down
31 changes: 31 additions & 0 deletions schema/compose-spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@
"domainname": {"type": "string"},
"entrypoint": {"$ref": "#/definitions/command"},
"env_file": {"$ref": "#/definitions/env_file"},
"label_file": {"$ref": "#/definitions/label_file"},
"environment": {"$ref": "#/definitions/list_or_dict"},

"expose": {
Expand Down Expand Up @@ -852,6 +853,36 @@
},

"env_file": {
"oneOf": [
{"type": "string"},
{
"type": "array",
"items": {
"oneOf": [
{"type": "string"},
{
"type": "object",
"additionalProperties": false,
"properties": {
"path": {
"type": "string"
},
"required": {
"type": ["boolean", "string"],
"default": true
}
},
"required": [
"path"
]
}
]
}
}
]
},

"label_file": {
"oneOf": [
{"type": "string"},
{
Expand Down
1 change: 1 addition & 0 deletions transform/canonical.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ func init() {
transformers["services.*.build.additional_contexts"] = transformKeyValue
transformers["services.*.depends_on"] = transformDependsOn
transformers["services.*.env_file"] = transformEnvFile
transformers["services.*.label_file"] = transformLabelFile
transformers["services.*.extends"] = transformExtends
transformers["services.*.networks"] = transformServiceNetworks
transformers["services.*.volumes.*"] = transformVolumeMount
Expand Down
55 changes: 55 additions & 0 deletions transform/labelfile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
Copyright 2020 The Compose Specification Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package transform

import (
"fmt"

"github.com/compose-spec/compose-go/v2/tree"
)

func transformLabelFile(data any, p tree.Path, _ bool) (any, error) {
switch v := data.(type) {
case string:
return []any{
transformLabelFileValue(v),
}, nil
case []any:
for i, e := range v {
v[i] = transformLabelFileValue(e)
}
return v, nil
default:
return nil, fmt.Errorf("%s: invalid type %T for label_file", p, v)
}
}

func transformLabelFileValue(data any) any {
switch v := data.(type) {
case string:
return map[string]any{
"path": v,
"required": true,
}
case map[string]any:
if _, ok := v["required"]; !ok {
v["required"] = true
}
return v
}
return nil
}
30 changes: 30 additions & 0 deletions types/derived.gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

47 changes: 47 additions & 0 deletions types/labelfile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
Copyright 2020 The Compose Specification Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package types

import (
"encoding/json"
)

type LabelFile struct {
Path string `yaml:"path,omitempty" json:"path,omitempty"`
Required bool `yaml:"required" json:"required"`
Format string `yaml:"format,omitempty" json:"format,omitempty"`
}

// MarshalYAML makes LabelFile implement yaml.Marshaler
func (e LabelFile) MarshalYAML() (interface{}, error) {
if e.Required {
return e.Path, nil
}
return map[string]any{
"path": e.Path,
"required": e.Required,
}, nil
}

// MarshalJSON makes LabelFile implement json.Marshaler
func (e *LabelFile) MarshalJSON() ([]byte, error) {
if e.Required {
return json.Marshal(e.Path)
}
// Pass as a value to avoid re-entering this method and use the default implementation
return json.Marshal(*e)
}
18 changes: 18 additions & 0 deletions types/labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ import (
// Labels is a mapping type for labels
type Labels map[string]string

func NewLabelsFromMappingWithEquals(mapping MappingWithEquals) Labels {
labels := Labels{}
for k, v := range mapping {
if v != nil {
labels[k] = *v
}
}
return labels
}

func (l Labels) Add(key, value string) Labels {
if l == nil {
l = Labels{}
Expand All @@ -42,6 +52,14 @@ func (l Labels) AsList() []string {
return s
}

func (l Labels) ToMappingWithEquals() MappingWithEquals {
mapping := MappingWithEquals{}
for k, v := range l {
mapping[k] = &v
}
return mapping
}

// label value can be a string | number | boolean | null (empty)
func labelValue(e interface{}) string {
if e == nil {
Expand Down
56 changes: 53 additions & 3 deletions types/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -633,22 +633,72 @@ func (p Project) WithServicesEnvironmentResolved(discardEnvFiles bool) (*Project
return newProject, nil
}

// WithServicesLabelsResolved parses label_files set for services to resolve the actual label map for services
// It returns a new Project instance with the changes and keep the original Project unchanged
func (p Project) WithServicesLabelsResolved(discardLabelFiles bool) (*Project, error) {
newProject := p.deepCopy()
for i, service := range newProject.Services {
labels := MappingWithEquals{}
// resolve variables based on other files we already parsed
var resolve dotenv.LookupFn = func(s string) (string, bool) {
v, ok := labels[s]
if ok && v != nil {
return *v, ok
}
return "", false
}

for _, labelFile := range service.LabelFiles {
vars, err := loadLabelFile(labelFile, resolve)
if err != nil {
return nil, err
}
labels.OverrideBy(vars.ToMappingWithEquals())
}

labels = labels.OverrideBy(service.Labels.ToMappingWithEquals())
service.Labels = NewLabelsFromMappingWithEquals(labels)

if discardLabelFiles {
service.LabelFiles = nil
}
newProject.Services[i] = service
}
return newProject, nil
}

func loadEnvFile(envFile EnvFile, resolve dotenv.LookupFn) (Mapping, error) {
if _, err := os.Stat(envFile.Path); os.IsNotExist(err) {
if envFile.Required {
return nil, fmt.Errorf("env file %s not found: %w", envFile.Path, err)
}
return nil, nil
}
file, err := os.Open(envFile.Path)

return loadMappingFile(envFile.Path, envFile.Format, resolve)
}

func loadLabelFile(labelFile LabelFile, resolve dotenv.LookupFn) (Mapping, error) {
if _, err := os.Stat(labelFile.Path); os.IsNotExist(err) {
if labelFile.Required {
return nil, fmt.Errorf("label file %s not found: %w", labelFile.Path, err)
}
return nil, nil
}

return loadMappingFile(labelFile.Path, labelFile.Format, resolve)
}

func loadMappingFile(path string, format string, resolve dotenv.LookupFn) (Mapping, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close() //nolint:errcheck

var fileVars map[string]string
if envFile.Format != "" {
fileVars, err = dotenv.ParseWithFormat(file, envFile.Path, resolve, envFile.Format)
if format != "" {
fileVars, err = dotenv.ParseWithFormat(file, path, resolve, format)
} else {
fileVars, err = dotenv.ParseWithLookup(file, resolve)
}
Expand Down
Loading

0 comments on commit ee29602

Please sign in to comment.