-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Add a tool to apply and validate config #4143
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
Merged
openshift-merge-robot
merged 1 commit into
openshift:master
from
petr-muller:applyconfig
Jun 24, 2019
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| # ApplyConfig | ||
|
|
||
| ApplyConfig is a tool for checking and applying cluster and service | ||
| configuration to the cluster. It behaves similarly to `oc apply -f directory/ --recursive` | ||
| but knows some additional DPTP conventions: | ||
|
|
||
| 1. Knows the distinction between admin resources and other resources | ||
| 2. Allows non-resources YAML files to be present | ||
| 3. Ignores directories and files that are marked by a convention | ||
| 4. Ignores JSON files | ||
|
|
||
| ## Usage | ||
|
|
||
| In general, `applyconfig --config-dir DIRECTORY` searches for all resource | ||
| config files under `DIRECTORY` and applies it. Subdirectories are searched | ||
| recursively and directories with names starting with `_` are skipped. Files and | ||
| directories are searched and applied in lexicographical order. All YAML files | ||
| are considered to be a config to apply, except those with filenames starting | ||
| with `_`. Files starting with `admin_` are considered to be admin resources, all | ||
| others are considered standard resources. | ||
|
|
||
| By default, `applyconfig` only runs in dry-mode, validating that eventual full | ||
| run would be successful. To issue a full run that actually commits the config | ||
| to the cluster, add a `--confirm=true` option. | ||
|
|
||
| By default, `applyconfig` works with non-admin resources. To apply admin | ||
| resources, use a `--level=admin` option. It is also possible to use | ||
| `--level=all` to apply both admin and standard resources. In this case, **all** | ||
| admin resources are applied first. | ||
|
|
||
| By default, standard resources are applied using user's standard credentials and | ||
| admin resources are attempted to apply as `system:admin`. It is possible to pass | ||
| the username to impersonate using the `--as=USER` option. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,189 @@ | ||
| package main | ||
|
|
||
| import ( | ||
| "flag" | ||
| "fmt" | ||
| "os" | ||
| "os/exec" | ||
| "path/filepath" | ||
| "regexp" | ||
| "strings" | ||
| ) | ||
|
|
||
| type level string | ||
|
|
||
| type options struct { | ||
| confirm bool | ||
| level level | ||
| user string | ||
| directory string | ||
| } | ||
|
|
||
| const ( | ||
| standardLevel level = "standard" | ||
| adminLevel level = "admin" | ||
| allLevel level = "all" | ||
| ) | ||
|
|
||
| const defaultAdminUser = "system:admin" | ||
|
|
||
| func (l level) isValid() bool { | ||
| return l == standardLevel || l == adminLevel || l == allLevel | ||
| } | ||
|
|
||
| func (l level) shouldApplyAdmin() bool { | ||
| return l == adminLevel || l == allLevel | ||
| } | ||
|
|
||
| func (l level) shouldApplyStandard() bool { | ||
| return l == standardLevel || l == allLevel | ||
| } | ||
|
|
||
| var adminConfig = regexp.MustCompile(`^admin_.+\.yaml$`) | ||
|
|
||
| func gatherOptions() *options { | ||
| opt := &options{} | ||
| var lvl string | ||
| flag.BoolVar(&opt.confirm, "confirm", false, "Set to true to make applyconfig commit the config to the cluster") | ||
| flag.StringVar(&lvl, "level", "standard", "Select which config to apply (standard, admin, all)") | ||
| flag.StringVar(&opt.user, "as", "", "Username to impersonate while applying the config") | ||
| flag.StringVar(&opt.directory, "config-dir", "", "Directory with config to apply") | ||
|
|
||
| opt.level = level(lvl) | ||
|
|
||
| flag.Parse() | ||
|
|
||
| if !opt.level.isValid() { | ||
| fmt.Fprintf(os.Stderr, "--level: must be one of [standard, admin, all]\n") | ||
| os.Exit(1) | ||
| } | ||
|
|
||
| if opt.directory == "" { | ||
| fmt.Fprintf(os.Stderr, "--config-dir must be provided\n") | ||
| os.Exit(1) | ||
| } | ||
|
|
||
| return opt | ||
| } | ||
|
|
||
| func isAdminConfig(filename string) bool { | ||
| return adminConfig.MatchString(filename) | ||
| } | ||
|
|
||
| func isStandardConfig(filename string) bool { | ||
| return filepath.Ext(filename) == ".yaml" && | ||
| !isAdminConfig(filename) | ||
| } | ||
|
|
||
| func makeOcArgs(path, user string, dry bool) []string { | ||
| args := []string{"apply", "-f", path} | ||
| if dry { | ||
| args = append(args, "--dry-run") | ||
| } | ||
|
|
||
| if user != "" { | ||
| args = append(args, "--as", user) | ||
| } | ||
|
|
||
| return args | ||
| } | ||
|
|
||
| func apply(path, user string, dry bool) error { | ||
| args := makeOcArgs(path, user, dry) | ||
|
|
||
| cmd := exec.Command("oc", args...) | ||
| if output, err := cmd.CombinedOutput(); err != nil { | ||
| if _, ok := err.(*exec.ExitError); ok { | ||
| fmt.Printf("[ERROR] oc %s: failed to apply\n%s\n", strings.Join(args, " "), string(output)) | ||
| } else { | ||
| fmt.Printf("[ERROR] oc %s: failed to execute: %v\n", strings.Join(args, " "), err) | ||
| } | ||
| return fmt.Errorf("failed to apply config") | ||
| } | ||
|
|
||
| fmt.Printf("oc %s: OK\n", strings.Join(args, " ")) | ||
| return nil | ||
| } | ||
|
|
||
| type processFn func(name, path string) error | ||
|
|
||
| func applyConfig(rootDir, cfgType string, process processFn) error { | ||
| failures := false | ||
| if err := filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error { | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| if info.IsDir() { | ||
| if strings.HasPrefix(info.Name(), "_") { | ||
| fmt.Printf("Skipping directory: %s\n", path) | ||
| return filepath.SkipDir | ||
| } | ||
| fmt.Printf("Applying %s config in directory: %s\n", cfgType, path) | ||
| return nil | ||
| } | ||
|
|
||
| if err := process(info.Name(), path); err != nil { | ||
| failures = true | ||
| } | ||
|
|
||
| return nil | ||
| }); err != nil { | ||
| // should not happen | ||
| fmt.Fprintf(os.Stderr, "failed to walk directory '%s': %v\n", rootDir, err) | ||
| return err | ||
| } | ||
|
|
||
| if failures { | ||
| return fmt.Errorf("failed to apply admin config") | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| func main() { | ||
| o := gatherOptions() | ||
| var adminErr, standardErr error | ||
|
|
||
| if o.level.shouldApplyAdmin() { | ||
| if o.user == "" { | ||
| o.user = defaultAdminUser | ||
| } | ||
|
|
||
| f := func(name, path string) error { | ||
| if !isAdminConfig(name) { | ||
| return nil | ||
| } | ||
| return apply(path, o.user, !o.confirm) | ||
| } | ||
|
|
||
| adminErr = applyConfig(o.directory, "admin", f) | ||
| if adminErr != nil { | ||
| fmt.Printf("There were failures while applying admin config\n") | ||
| } | ||
| } | ||
|
|
||
| if o.level.shouldApplyStandard() { | ||
| f := func(name, path string) error { | ||
| if !isStandardConfig(name) { | ||
| return nil | ||
| } | ||
| if strings.HasPrefix(name, "_") { | ||
| return nil | ||
| } | ||
|
|
||
| return apply(path, o.user, !o.confirm) | ||
| } | ||
|
|
||
| standardErr = applyConfig(o.directory, "standard", f) | ||
| if standardErr != nil { | ||
| fmt.Printf("There were failures while applying standard config\n") | ||
| } | ||
| } | ||
|
|
||
| if standardErr != nil || adminErr != nil { | ||
| os.Exit(1) | ||
| } | ||
|
|
||
| fmt.Printf("Success!\n") | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| package main | ||
|
|
||
| import ( | ||
| "reflect" | ||
| "testing" | ||
| ) | ||
|
|
||
| func TestIsAdminConfig(t *testing.T) { | ||
| testCases := []struct{ | ||
| filename string | ||
| expected bool | ||
| }{ | ||
| { | ||
| filename: "admin_01_something_rbac.yaml", | ||
| expected: true, | ||
| }, | ||
| { | ||
| filename: "admin_something_rbac.yaml", | ||
| expected: true, | ||
| }, | ||
| // Negative | ||
| { filename: "cfg_01_something" }, | ||
| { filename: "admin_01_something_rbac" }, | ||
| { filename: "admin_01_something_rbac.yml" }, | ||
| { filename: "admin.yaml" }, | ||
| } | ||
|
|
||
| for _, tc := range testCases { | ||
| t.Run(tc.filename, func(t *testing.T){ | ||
| is := isAdminConfig(tc.filename) | ||
| if is != tc.expected { | ||
| t.Errorf("expected %t, got %t", tc.expected, is) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestIsStandardConfig(t *testing.T) { | ||
| testCases := []struct{ | ||
| filename string | ||
| expected bool | ||
| }{ | ||
| { | ||
| filename: "01_something_rbac.yaml", | ||
| expected: true, | ||
| }, | ||
| { | ||
| filename: "something_rbac.yaml", | ||
| expected: true, | ||
| }, | ||
| // Negative | ||
| { filename: "admin_01_something.yaml" }, | ||
| { filename: "cfg_01_something_rbac" }, | ||
| { filename: "cfg_01_something_rbac.yml" }, | ||
| } | ||
|
|
||
| for _, tc := range testCases { | ||
| t.Run(tc.filename, func(t *testing.T){ | ||
| is := isStandardConfig(tc.filename) | ||
| if is != tc.expected { | ||
| t.Errorf("expected %t, got %t", tc.expected, is) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestMakeOcArgs(t *testing.T) { | ||
| testCases := []struct{ | ||
| name string | ||
|
|
||
| path string | ||
| user string | ||
| dry bool | ||
|
|
||
| expected []string | ||
| }{ | ||
| { | ||
| name: "no user, not dry", | ||
| path: "/path/to/file", | ||
| expected: []string{"apply", "-f", "/path/to/file"}, | ||
| }, | ||
| { | ||
| name: "no user, dry", | ||
| path: "/path/to/different/file", | ||
| dry: true, | ||
| expected: []string{"apply", "-f", "/path/to/different/file", "--dry-run"}, | ||
| }, | ||
| { | ||
| name: "user, dry", | ||
| path: "/path/to/file", | ||
| dry: true, | ||
| user: "joe", | ||
| expected: []string{"apply", "-f", "/path/to/file", "--dry-run", "--as", "joe"}, | ||
| }, | ||
| { | ||
| name: "user, not dry", | ||
| path: "/path/to/file", | ||
| user: "joe", | ||
| expected: []string{"apply", "-f", "/path/to/file", "--as", "joe"}, | ||
| }, | ||
| } | ||
|
|
||
| for _, tc := range testCases { | ||
| t.Run(tc.name, func(t *testing.T){ | ||
| args := makeOcArgs(tc.path, tc.user, tc.dry) | ||
| if !reflect.DeepEqual(args, tc.expected) { | ||
| t.Errorf("Expected '%v', got '%v'", tc.expected, args) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.