Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PoC: Global/Manifest-level Parameters #1538

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,5 @@ environmentGroups:
tokenEndpoint:
type: environment
value: OAUTH_TOKEN_ENDPOINT
parameters:
hansi: hinterseer
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ configs:
configId: profile
property: id
type: reference
environment: Env1
environment:
type: reference
property: _global:environment
owner:
type: reference
property: _global:hansi
template: subfolder/slack.json
skip: false
- id: email
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@
"active": true,
"url": "https://hooks.slack.com/services/A/B/C",
"channel": "#team-bas-ops",
"title": "{{ .environment }} stage: {State} {ProblemID} \n{ProblemURL}\n{ProblemTitle}\n{ProblemImpact} {ProblemSeverity}\n----\n{ProblemDetailsText}\n"
}
"title": "{{ .environment }} {{.owner}} stage: {State} {ProblemID} \n{ProblemURL}\n{ProblemTitle}\n{ProblemImpact} {ProblemSeverity}\n----\n{ProblemDetailsText}\n"
}
2 changes: 1 addition & 1 deletion pkg/config/entities/resolvedentity.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ type ResolvedEntity struct {
// If the key is found, the function returns the associated value and true. If the
// key is not found, it returns nil and false.
func ResolvePropValue(key string, props map[any]any) (any, bool) {
first, rest, _ := str.Cut(key, ".")
first, rest, _ := str.Cut(key, ".") // THIS makes it impossible to define global params as '.global.<key>' as they're treated as map separators.
if p, f := props[first]; f {
if rest == "" {
return p, true
Expand Down
2 changes: 2 additions & 0 deletions pkg/manifest/internal/persistence/manifest_persitence.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ type Manifest struct {
EnvironmentGroups []Group `yaml:"environmentGroups" json:"environmentGroups" jsonschema:"minItems=1,description=A list of environment groups that configs in the defined 'projects' will be deployed to. Required when deploying environment configurations."`
// Accounts is a list of accounts that account resources in Projects will be deployed to
Accounts []Account `yaml:"accounts,omitempty" json:"accounts" jsonschema:"minItems=1,description=A list of of accounts that account resources defined in 'projects' will be deployed to. Required when deploying account resources."`

Parameters map[string]interface{} `yaml:"parameters,omitempty" json:"parameters"`
}

type Account struct {
Expand Down
17 changes: 15 additions & 2 deletions pkg/manifest/loader/manifest_loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"github.com/dynatrace/dynatrace-configuration-as-code/v2/internal/log/field"
"github.com/dynatrace/dynatrace-configuration-as-code/v2/internal/secret"
version2 "github.com/dynatrace/dynatrace-configuration-as-code/v2/internal/version"
"github.com/dynatrace/dynatrace-configuration-as-code/v2/pkg/config"
"github.com/dynatrace/dynatrace-configuration-as-code/v2/pkg/manifest"
"github.com/dynatrace/dynatrace-configuration-as-code/v2/pkg/manifest/internal/persistence"
"github.com/dynatrace/dynatrace-configuration-as-code/v2/pkg/version"
Expand Down Expand Up @@ -194,16 +195,28 @@ func Load(context *Context) (manifest.Manifest, []error) {
errs = append(errs, newManifestLoaderError(context.ManifestPath, accErr.Error()))
}

// params
params, paramErrs := parseParameters(context.Fs, config.DefaultParameterParsers, manifestYAML.Parameters)
if paramErrs != nil {
errs = append(errs, newManifestLoaderError(context.ManifestPath, paramErrs.Error()))
}

// if any errors occurred up to now, return them
if errs != nil {
return manifest.Manifest{}, errs
}

return manifest.Manifest{
m := manifest.Manifest{
Projects: projectDefinitions,
Environments: environmentDefinitions,
Accounts: accounts,
}, nil
}

if len(params) > 0 {
m.Parameters = params
}

return m, nil
}

func parseAuth(context *Context, a persistence.Auth) (manifest.Auth, error) {
Expand Down
137 changes: 137 additions & 0 deletions pkg/manifest/loader/manifest_loader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ package loader
import (
"fmt"
monacoVersion "github.com/dynatrace/dynatrace-configuration-as-code/v2/internal/version"
"github.com/dynatrace/dynatrace-configuration-as-code/v2/pkg/config/parameter"
"github.com/dynatrace/dynatrace-configuration-as-code/v2/pkg/config/parameter/environment"
valueParam "github.com/dynatrace/dynatrace-configuration-as-code/v2/pkg/config/parameter/value"
"github.com/dynatrace/dynatrace-configuration-as-code/v2/pkg/manifest"
"github.com/dynatrace/dynatrace-configuration-as-code/v2/pkg/manifest/internal/persistence"
"github.com/dynatrace/dynatrace-configuration-as-code/v2/pkg/version"
Expand Down Expand Up @@ -643,6 +646,65 @@ environmentGroups:
},
},
},
{
name: "unmarshal manifest with parameters",
given: `
manifestVersion: "1.0"
projects:
- name: project
environmentGroups:
- name: default
environments:
- name: env
url:
type: environment
value: ENV_URL
auth:
token:
name: ENV_TOKEN
parameters:
owner: hansi
department:
type: environment
name: ENV_VAR
`,
expected: expected{
manifest: persistence.Manifest{
ManifestVersion: "1.0",
Projects: []persistence.Project{
{
Name: "project",
},
},
EnvironmentGroups: []persistence.Group{
{
Name: "default",
Environments: []persistence.Environment{
{
Name: "env",
URL: persistence.TypedValue{
Type: persistence.TypeEnvironment,
Value: "ENV_URL",
},
Auth: persistence.Auth{
Token: persistence.AuthSecret{
Name: "ENV_TOKEN",
},
},
},
},
},
},
Parameters: map[string]interface{}{
"owner": "hansi",
"department": map[interface{}]interface{}{
"type": "environment",
"name": "ENV_VAR",
},
},
},
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
Expand Down Expand Up @@ -1149,6 +1211,81 @@ environmentGroups:
Accounts: map[string]manifest.Account{},
},
},
{
name: "Valid with parameters",
manifestContent: `
manifestVersion: "1.0"
projects:
- name: project
environmentGroups:
- name: default
environments:
- name: env
url: "https://test.test"
auth:
token:
name: e
parameters:
owner: hansi
department:
type: environment
name: test-env-var
`,
errsContain: []string{},
expectedManifest: manifest.Manifest{
Projects: map[string]manifest.ProjectDefinition{
"project": {
Name: "project",
Path: "project",
},
},
Environments: map[string]manifest.EnvironmentDefinition{
"env": {
Group: "default",
Name: "env",
URL: manifest.URLDefinition{
Type: manifest.ValueURLType,
Value: "https://test.test",
},
Auth: manifest.Auth{
Token: manifest.AuthSecret{
Name: "e",
Value: "mock token",
},
},
},
},
Accounts: map[string]manifest.Account{},
Parameters: map[string]parameter.Parameter{
"owner": valueParam.New("hansi"),
"department": environment.New("test-env-var"),
},
},
},
{
name: "Invalid global parameter types produce error",
manifestContent: `
manifestVersion: "1.0"
projects:
- name: project
environmentGroups:
- name: default
environments:
- name: env
url: "https://test.test"
auth:
token:
name: e
parameters:
department:
type: reference
project: project
configType: application-mobile
configId: monaco-has-no-clue-I-exist-yet
`,
errsContain: []string{"invalid parameter type"},
expectedManifest: manifest.Manifest{},
},
{
name: "Missing group errors",
envs: []string{"envA", "envB"},
Expand Down
85 changes: 85 additions & 0 deletions pkg/manifest/loader/parameters.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* @license
* Copyright 2023 Dynatrace LLC
* 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 loader

import (
"fmt"
"github.com/dynatrace/dynatrace-configuration-as-code/v2/internal/maps"
"github.com/dynatrace/dynatrace-configuration-as-code/v2/internal/mutlierror"
"github.com/dynatrace/dynatrace-configuration-as-code/v2/pkg/config/parameter"
"github.com/dynatrace/dynatrace-configuration-as-code/v2/pkg/config/parameter/compound"
"github.com/dynatrace/dynatrace-configuration-as-code/v2/pkg/config/parameter/reference"
valueParam "github.com/dynatrace/dynatrace-configuration-as-code/v2/pkg/config/parameter/value"
"github.com/spf13/afero"
)

type ParamTypeParsers = map[string]parameter.ParameterSerDe

func parseParameters(fs afero.Fs, parsers ParamTypeParsers, in map[string]interface{}) (map[string]parameter.Parameter, error) {

parameters := make(map[string]parameter.Parameter)
var errs []error

for name, param := range in {
if _, found := parameters[name]; found {
continue
}

result, err := parseParameter(fs, parsers, name, param)
if err != nil {
errs = append(errs, err)
continue
}

parameters[name] = result
}

if errs != nil {
return nil, mutlierror.New(errs...)
}

return parameters, nil
}

func parseParameter(fs afero.Fs, parsers ParamTypeParsers, name string, param interface{}) (parameter.Parameter, error) {

if val, ok := param.(map[interface{}]interface{}); ok {
parameterType := toString(val["type"])

if parameterType == reference.ReferenceParameterType || parameterType == compound.CompoundParameterType {
return nil, fmt.Errorf("invalid parameter type `%s` for global parameter %q", parameterType, name)
}

serDe, found := parsers[parameterType]

if !found {
return nil, fmt.Errorf("unknown parameter type `%s` for global parameter %q", parameterType, name)
}

return serDe.Deserializer(parameter.ParameterParserContext{
Fs: fs,
ParameterName: name,
Value: maps.ToStringMap(val),
})
}

return valueParam.New(param), nil
}

func toString(v interface{}) string {
return fmt.Sprintf("%v", v)
}
3 changes: 3 additions & 0 deletions pkg/manifest/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ package manifest
import (
"fmt"
"github.com/dynatrace/dynatrace-configuration-as-code/v2/internal/secret"
"github.com/dynatrace/dynatrace-configuration-as-code/v2/pkg/config/parameter"
"github.com/dynatrace/dynatrace-configuration-as-code/v2/pkg/oauth2/endpoints"
"github.com/google/uuid"
"golang.org/x/exp/maps"
Expand Down Expand Up @@ -137,4 +138,6 @@ type Manifest struct {

// Accounts holds all accounts defined in the manifest. Key is the user-defined account name.
Accounts map[string]Account

Parameters map[string]parameter.Parameter
}
11 changes: 11 additions & 0 deletions pkg/project/v2/project_loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,17 @@
errs = append(errs, configErrs...)
configs = append(configs, loadedConfigs...)
}

// TODO ugly and produces memory overhead, but the easiest way to make global variables available is putting them right in the config

Check failure on line 316 in pkg/project/v2/project_loader.go

View workflow job for this annotation

GitHub Actions / golangci

[golangci] pkg/project/v2/project_loader.go#L316

pkg/project/v2/project_loader.go:316: Line contains TODO/BUG/FIXME: "TODO ugly and produces memory overhead, ..." (godox)
Raw output
pkg/project/v2/project_loader.go:316: pkg/project/v2/project_loader.go:316: Line contains TODO/BUG/FIXME: "TODO ugly and produces memory overhead, ..." (godox)
	// TODO ugly and produces memory overhead, but the easiest way to make global variables available is putting them right in the config
// where they can then be referenced via ref params to "property: "_global:<key>""
// Rather than magic strings, a proper implementation should IMO introduce a 'global' parameter type for configs and make access explicit
for i, _ := range configs {
for k, v := range loadingContext.Manifest.Parameters {
configs[i].Parameters["_global:"+k] = v
}
configs[i].Parameters["_global:environment"] = value.New(configs[i].Environment)
}

return configs, errs
}

Expand Down
Loading