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

[ansible]: mark variables from custom resources as unsafe #4566

Merged
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions changelog/fragments/mark-variables-unsafe.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
entries:
- description: >
Mark the input variables from custom resources as unsafe by default in Ansible operators.
kind: bugfix
breaking: false
36 changes: 36 additions & 0 deletions internal/ansible/runner/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ func New(watch watches.Watch, runnerArgs string) (Runner, error) {
ansibleVerbosity: watch.AnsibleVerbosity,
ansibleArgs: runnerArgs,
snakeCaseParameters: watch.SnakeCaseParameters,
markUnsafe: watch.MarkUnsafe,
}, nil
}

Expand All @@ -174,6 +175,7 @@ type runner struct {
maxRunnerArtifacts int
ansibleVerbosity int
snakeCaseParameters bool
markUnsafe bool
ansibleArgs string
}

Expand Down Expand Up @@ -341,6 +343,12 @@ func (r *runner) makeParameters(u *unstructured.Unstructured) map[string]interfa
}
}

if r.markUnsafe {
for key, val := range parameters {
parameters[key] = markUnsafe(val)
}
}

parameters["ansible_operator_meta"] = map[string]string{"namespace": u.GetNamespace(), "name": u.GetName()}

objKey := escapeAnsibleKey(fmt.Sprintf("_%v_%v", r.GVK.Group, strings.ToLower(r.GVK.Kind)))
Expand All @@ -360,6 +368,34 @@ func (r *runner) makeParameters(u *unstructured.Unstructured) map[string]interfa
return parameters
}

// markUnsafe recursively checks for string values and marks them unsafe.
// for eg:
// spec:
// key: "val"
// would be marked unsafe in JSON format as:
// spec:
// key: map{__ansible_unsafe:"val"}
func markUnsafe(values interface{}) interface{} {
switch v := values.(type) {
case []interface{}:
var p []interface{}
for _, n := range v {
p = append(p, markUnsafe(n))
}
return p
case map[string]interface{}:
m := make(map[string]interface{})
for k, v := range v {
m[k] = markUnsafe(v)
}
return m
case string:
return map[string]interface{}{"__ansible_unsafe": values}
default:
return values
}
}

// escapeAnsibleKey - replaces characters that would result in an inaccessible Ansible parameter with underscores
// ie, _cert-manager.k8s.io would be converted to _cert_manager_k8s_io
func escapeAnsibleKey(key string) string {
Expand Down
127 changes: 127 additions & 0 deletions internal/ansible/runner/runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,3 +244,130 @@ func TestAnsibleVerbosityString(t *testing.T) {
}
}
}

func TestMakeParameters(t *testing.T) {
var (
inputSpec string = "testKey"
)

testCases := []struct {
name string
inputParams unstructured.Unstructured
expectedSafeParams interface{}
}{
{
name: "should mark values passed as string unsafe",
inputParams: unstructured.Unstructured{
Object: map[string]interface{}{
"spec": map[string]interface{}{
inputSpec: "testVal",
},
},
},
expectedSafeParams: map[string]interface{}{
"__ansible_unsafe": "testVal",
},
},
{
name: "should not mark integers unsafe",
inputParams: unstructured.Unstructured{
Object: map[string]interface{}{
"spec": map[string]interface{}{
inputSpec: 3,
},
},
},
expectedSafeParams: 3,
},
{
name: "should recursively mark values in dictionary as unsafe",
inputParams: unstructured.Unstructured{
Object: map[string]interface{}{
"spec": map[string]interface{}{
inputSpec: map[string]interface{}{
"testsubKey1": "val1",
"testsubKey2": "val2",
},
},
},
},
expectedSafeParams: map[string]interface{}{
"testsubKey1": map[string]interface{}{
"__ansible_unsafe": "val1",
},
"testsubKey2": map[string]interface{}{
"__ansible_unsafe": "val2",
},
},
},
{
name: "should recursively mark values in list as unsafe",
inputParams: unstructured.Unstructured{
Object: map[string]interface{}{
"spec": map[string]interface{}{
inputSpec: []interface{}{
"testVal1",
"testVal2",
},
},
},
},
expectedSafeParams: []interface{}{
map[string]interface{}{
"__ansible_unsafe": "testVal1",
},
map[string]interface{}{
"__ansible_unsafe": "testVal2",
},
},
},
{
name: "should recursively mark values in list/dict as unsafe",
inputParams: unstructured.Unstructured{
Object: map[string]interface{}{
"spec": map[string]interface{}{
inputSpec: []interface{}{
"testVal1",
"testVal2",
map[string]interface{}{
"testVal3": 3,
"testVal4": "__^&{__)",
},
},
},
},
},
expectedSafeParams: []interface{}{
map[string]interface{}{
"__ansible_unsafe": "testVal1",
},
map[string]interface{}{
"__ansible_unsafe": "testVal2",
},
map[string]interface{}{
"testVal3": 3,
"testVal4": map[string]interface{}{
"__ansible_unsafe": "__^&{__)",
},
},
},
},
}

for _, tc := range testCases {
testRunner := runner{
markUnsafe: true,
}
parameters := testRunner.makeParameters(&tc.inputParams)

val, ok := parameters[inputSpec]
if !ok {
t.Fatalf("Error occurred, value %s in spec is missing", inputSpec)
} else {
eq := reflect.DeepEqual(val, tc.expectedSafeParams)
if !eq {
t.Errorf("Error occurred, parameters %v are not marked unsafe", val)
}
}
}
}
6 changes: 6 additions & 0 deletions internal/ansible/watches/testdata/valid.yaml.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
kind: NoFinalizer
playbook: {{ .ValidPlaybook }}
reconcilePeriod: 2s
- version: v1alpha1
group: app.example.com
kind: WithUnsafeMarked
playbook: {{ .ValidPlaybook }}
reconcilePeriod: 2s
markUnsafe: True
- version: v1alpha1
group: app.example.com
kind: Playbook
Expand Down
9 changes: 9 additions & 0 deletions internal/ansible/watches/watches.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ type Watch struct {
WatchDependentResources bool `yaml:"watchDependentResources"`
WatchClusterScopedResources bool `yaml:"watchClusterScopedResources"`
SnakeCaseParameters bool `yaml:"snakeCaseParameters"`
MarkUnsafe bool `yaml:"markUnsafe"`
Selector metav1.LabelSelector `yaml:"selector"`

// Not configurable via watches.yaml
Expand All @@ -76,6 +77,7 @@ var (
watchDependentResourcesDefault = true
watchClusterScopedResourcesDefault = false
snakeCaseParametersDefault = true
markUnsafeDefault = false
selectorDefault = metav1.LabelSelector{}

// these are overridden by cmdline flags
Expand Down Expand Up @@ -127,6 +129,7 @@ type alias struct {
WatchDependentResources *bool `yaml:"watchDependentResources,omitempty"`
WatchClusterScopedResources *bool `yaml:"watchClusterScopedResources,omitempty"`
SnakeCaseParameters *bool `yaml:"snakeCaseParameters"`
MarkUnsafe *bool `yaml:"markUnsafe"`
Blacklist []schema.GroupVersionKind `yaml:"blacklist,omitempty"`
Finalizer *Finalizer `yaml:"finalizer"`
Selector tempLabelSelector `yaml:"selector"`
Expand Down Expand Up @@ -162,6 +165,10 @@ func (w *Watch) setValuesFromAlias(tmp alias) error {
tmp.SnakeCaseParameters = &snakeCaseParametersDefault
}

if tmp.MarkUnsafe == nil {
tmp.MarkUnsafe = &markUnsafeDefault
}

gvk := schema.GroupVersionKind{
Group: tmp.Group,
Version: tmp.Version,
Expand All @@ -183,6 +190,7 @@ func (w *Watch) setValuesFromAlias(tmp alias) error {
w.ManageStatus = *tmp.ManageStatus
w.WatchDependentResources = *tmp.WatchDependentResources
w.SnakeCaseParameters = *tmp.SnakeCaseParameters
w.MarkUnsafe = *tmp.MarkUnsafe
w.WatchClusterScopedResources = *tmp.WatchClusterScopedResources
w.Finalizer = tmp.Finalizer
w.AnsibleVerbosity = getAnsibleVerbosity(gvk, ansibleVerbosityDefault)
Expand Down Expand Up @@ -316,6 +324,7 @@ func New(gvk schema.GroupVersionKind, role, playbook string, vars map[string]int
WatchDependentResources: watchDependentResourcesDefault,
WatchClusterScopedResources: watchClusterScopedResourcesDefault,
SnakeCaseParameters: snakeCaseParametersDefault,
MarkUnsafe: markUnsafeDefault,
Finalizer: finalizer,
AnsibleVerbosity: ansibleVerbosityDefault,
Selector: selectorDefault,
Expand Down
19 changes: 19 additions & 0 deletions internal/ansible/watches/watches_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ func TestNew(t *testing.T) {
t.Fatalf("Unexpected snakeCaseParameters %v expected %v", watch.SnakeCaseParameters,
snakeCaseParametersDefault)
}
if watch.MarkUnsafe != markUnsafeDefault {
t.Fatalf("Unexpected markUnsafe %v expected %v", watch.MarkUnsafe, markUnsafeDefault)
}
if watch.WatchClusterScopedResources != watchClusterScopedResourcesDefault {
t.Fatalf("Unexpected watchClusterScopedResources %v expected %v",
watch.WatchClusterScopedResources, watchClusterScopedResourcesDefault)
Expand Down Expand Up @@ -147,6 +150,18 @@ func TestLoad(t *testing.T) { //nolint:gocyclo
WatchDependentResources: true,
WatchClusterScopedResources: false,
SnakeCaseParameters: true,
MarkUnsafe: false,
},
Watch{
GroupVersionKind: schema.GroupVersionKind{
Version: "v1alpha1",
Group: "app.example.com",
Kind: "WithUnsafeMarked",
},
Playbook: validTemplate.ValidPlaybook,
ManageStatus: true,
ReconcilePeriod: twoSeconds,
MarkUnsafe: true,
},
Watch{
GroupVersionKind: schema.GroupVersionKind{
Expand Down Expand Up @@ -536,6 +551,10 @@ func TestLoad(t *testing.T) { //nolint:gocyclo
t.Fatalf("The GVK: %v unexpected reconcile period: %v expected reconcile period: %v", gvk,
gotWatch.ReconcilePeriod, expectedWatch.ReconcilePeriod)
}
if gotWatch.MarkUnsafe != expectedWatch.MarkUnsafe {
t.Fatalf("The GVK: %v unexpected mark unsafe: %v expected mark unsafe: %v", gvk,
gotWatch.MarkUnsafe, expectedWatch.MarkUnsafe)
}

for i, val := range expectedWatch.Blacklist {
if val != gotWatch.Blacklist[i] {
Expand Down