Skip to content

Commit

Permalink
controller/bootstrap: use files with multiple yaml documents
Browse files Browse the repository at this point in the history
Users can push manifests during bootstrap that of the form:

```yaml
---
```

Especially for the installer: setting authorizes_keys [1] and setting hyperthreading [2] will push a manifest that includes multiple machineconfig objects for
control-plane (master) and compute (worker) roles.

Single file with multiple k8s objects separated by `---` is also a supported structure for `oc create|apply` ie. there is a high chance that users trying to push machineconfigs at
install time might create such files.

This commit allows bootstrap controller to read all k8s objects, even ones described above to find all the `machineconfiguration.openshift.io` Objects.

[1]: openshift/installer#1150
[2]: openshift/installer#1392
  • Loading branch information
abhinavdahiya committed Mar 26, 2019
1 parent c83a2df commit 7273740
Show file tree
Hide file tree
Showing 2 changed files with 191 additions and 16 deletions.
88 changes: 72 additions & 16 deletions pkg/controller/bootstrap/bootstrap.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package bootstrap

import (
"bytes"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
Expand All @@ -10,6 +13,7 @@ import (
"github.com/golang/glog"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
yamlutil "k8s.io/apimachinery/pkg/util/yaml"
kscheme "k8s.io/client-go/kubernetes/scheme"

"github.com/openshift/machine-config-operator/pkg/apis/machineconfiguration.openshift.io/v1"
Expand Down Expand Up @@ -63,28 +67,35 @@ func (b *Bootstrap) Run(destDir string) error {
continue
}

path := filepath.Join(b.manifestDir, info.Name())
raw, err := ioutil.ReadFile(path)
file, err := os.Open(filepath.Join(b.manifestDir, info.Name()))
if err != nil {
return err
return fmt.Errorf("error opening %s: %v", file.Name(), err)
}
defer file.Close()

obji, err := runtime.Decode(scheme.Codecs.UniversalDecoder(v1.SchemeGroupVersion), raw)
manifests, err := parseManifests(file.Name(), file)
if err != nil {
glog.V(4).Infof("skipping path because of error: %v", err)
// don't care
continue
return fmt.Errorf("error parsing manifests from %s: %v", file.Name(), err)
}

switch obj := obji.(type) {
case *v1.MachineConfigPool:
pools = append(pools, obj)
case *v1.MachineConfig:
configs = append(configs, obj)
case *v1.ControllerConfig:
cconfig = obj
default:
glog.Infof("skipping %q %T", path, obji)
for idx, m := range manifests {
obji, err := runtime.Decode(scheme.Codecs.UniversalDecoder(v1.SchemeGroupVersion), m.Raw)
if err != nil {
glog.V(4).Infof("skipping path %q [%d] manifest because of error: %v", file.Name(), idx+1, err)
// don't care
continue
}

switch obj := obji.(type) {
case *v1.MachineConfigPool:
pools = append(pools, obj)
case *v1.MachineConfig:
configs = append(configs, obj)
case *v1.ControllerConfig:
cconfig = obj
default:
glog.Infof("skipping %q [%d] manifest because %T", file.Name(), idx+1, obji)
}
}
}

Expand Down Expand Up @@ -148,3 +159,48 @@ func getPullSecretFromSecret(sData []byte) ([]byte, error) {
}
return s.Data[corev1.DockerConfigJsonKey], nil
}

type manifest struct {
Raw []byte
}

// UnmarshalJSON unmarshals bytes of single kubernetes object to manifest.
func (m *manifest) UnmarshalJSON(in []byte) error {
if m == nil {
return errors.New("Manifest: UnmarshalJSON on nil pointer")
}

// This happens when marshalling
// <yaml>
// --- (this between two `---`)
// ---
// <yaml>
if bytes.Equal(in, []byte("null")) {
m.Raw = nil
return nil
}

m.Raw = append(m.Raw[0:0], in...)
return nil
}

// parseManifests parses a YAML or JSON document that may contain one or more
// kubernetes resources.
func parseManifests(filename string, r io.Reader) ([]manifest, error) {
d := yamlutil.NewYAMLOrJSONDecoder(r, 1024)
var manifests []manifest
for {
m := manifest{}
if err := d.Decode(&m); err != nil {
if err == io.EOF {
return manifests, nil
}
return manifests, fmt.Errorf("error parsing %q: %v", filename, err)
}
m.Raw = bytes.TrimSpace(m.Raw)
if len(m.Raw) == 0 || bytes.Equal(m.Raw, []byte("null")) {
continue
}
manifests = append(manifests, m)
}
}
119 changes: 119 additions & 0 deletions pkg/controller/bootstrap/bootstrap_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package bootstrap

import (
"reflect"
"strings"
"testing"

"k8s.io/apimachinery/pkg/util/diff"
)

func TestParseManifests(t *testing.T) {
tests := []struct {
name string
raw string
want []manifest
}{{
name: "ingress",
raw: `
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: test-ingress
namespace: test-namespace
spec:
rules:
- http:
paths:
- path: /testpath
backend:
serviceName: test
servicePort: 80
`,
want: []manifest{{
Raw: []byte(`{"apiVersion":"extensions/v1beta1","kind":"Ingress","metadata":{"name":"test-ingress","namespace":"test-namespace"},"spec":{"rules":[{"http":{"paths":[{"backend":{"serviceName":"test","servicePort":80},"path":"/testpath"}]}}]}}`),
}},
}, {
name: "two-resources",
raw: `
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: test-ingress
namespace: test-namespace
spec:
rules:
- http:
paths:
- path: /testpath
backend:
serviceName: test
servicePort: 80
---
apiVersion: v1
kind: ConfigMap
metadata:
name: a-config
namespace: default
data:
color: "red"
multi-line: |
hello world
how are you?
`,
want: []manifest{{
Raw: []byte(`{"apiVersion":"extensions/v1beta1","kind":"Ingress","metadata":{"name":"test-ingress","namespace":"test-namespace"},"spec":{"rules":[{"http":{"paths":[{"backend":{"serviceName":"test","servicePort":80},"path":"/testpath"}]}}]}}`),
}, {
Raw: []byte(`{"apiVersion":"v1","data":{"color":"red","multi-line":"hello world\nhow are you?\n"},"kind":"ConfigMap","metadata":{"name":"a-config","namespace":"default"}}`),
}},
}, {
name: "two-resources-with-empty",
raw: `
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: test-ingress
namespace: test-namespace
spec:
rules:
- http:
paths:
- path: /testpath
backend:
serviceName: test
servicePort: 80
---
---
apiVersion: v1
kind: ConfigMap
metadata:
name: a-config
namespace: default
data:
color: "red"
multi-line: |
hello world
how are you?
---
`,
want: []manifest{{
Raw: []byte(`{"apiVersion":"extensions/v1beta1","kind":"Ingress","metadata":{"name":"test-ingress","namespace":"test-namespace"},"spec":{"rules":[{"http":{"paths":[{"backend":{"serviceName":"test","servicePort":80},"path":"/testpath"}]}}]}}`),
}, {
Raw: []byte(`{"apiVersion":"v1","data":{"color":"red","multi-line":"hello world\nhow are you?\n"},"kind":"ConfigMap","metadata":{"name":"a-config","namespace":"default"}}`),
}},
}}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, err := parseManifests("dummy-file-name", strings.NewReader(test.raw))
if err != nil {
t.Fatalf("failed to parse manifest: %v", err)
}

if !reflect.DeepEqual(got, test.want) {
t.Fatalf("mismatch found %s", diff.ObjectDiff(got, test.want))
}
})
}

}

0 comments on commit 7273740

Please sign in to comment.