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
33 changes: 33 additions & 0 deletions tools/applyconfig/README.md
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.
189 changes: 189 additions & 0 deletions tools/applyconfig/applyconfig.go
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")
}
112 changes: 112 additions & 0 deletions tools/applyconfig/applyconfig_test.go
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)
}
})
}
}