Merge pull request #9759 from EduardoVega/9129-multi-docs-kube

Support multi doc yaml for generate/play kube
This commit is contained in:
OpenShift Merge Robot
2021-03-25 10:44:03 -07:00
committed by GitHub
6 changed files with 413 additions and 79 deletions

2
go.mod
View File

@ -66,7 +66,7 @@ require (
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776
k8s.io/api v0.20.5
k8s.io/apimachinery v0.20.5
)

View File

@ -44,11 +44,10 @@ func (ic *ContainerEngine) GenerateSystemd(ctx context.Context, nameOrID string,
func (ic *ContainerEngine) GenerateKube(ctx context.Context, nameOrIDs []string, options entities.GenerateKubeOptions) (*entities.GenerateKubeReport, error) {
var (
pods []*libpod.Pod
podYAML *k8sAPI.Pod
err error
ctrs []*libpod.Container
servicePorts []k8sAPI.ServicePort
serviceYAML k8sAPI.Service
kubePods []*k8sAPI.Pod
kubeServices []k8sAPI.Service
content []byte
)
for _, nameOrID := range nameOrIDs {
// Get the container in question
@ -59,9 +58,6 @@ func (ic *ContainerEngine) GenerateKube(ctx context.Context, nameOrIDs []string,
return nil, err
}
pods = append(pods, pod)
if len(pods) > 1 {
return nil, errors.New("can only generate single pod at a time")
}
} else {
if len(ctr.Dependencies()) > 0 {
return nil, errors.Wrapf(define.ErrNotImplemented, "containers with dependencies")
@ -79,20 +75,29 @@ func (ic *ContainerEngine) GenerateKube(ctx context.Context, nameOrIDs []string,
return nil, errors.New("cannot generate pods and containers at the same time")
}
if len(pods) == 1 {
podYAML, servicePorts, err = pods[0].GenerateForKube()
if len(pods) >= 1 {
pos, svcs, err := getKubePods(pods, options.Service)
if err != nil {
return nil, err
}
kubePods = append(kubePods, pos...)
if options.Service {
kubeServices = append(kubeServices, svcs...)
}
} else {
podYAML, err = libpod.GenerateForKube(ctrs)
}
if err != nil {
return nil, err
po, err := libpod.GenerateForKube(ctrs)
if err != nil {
return nil, err
}
kubePods = append(kubePods, po)
if options.Service {
kubeServices = append(kubeServices, libpod.GenerateKubeServiceFromV1Pod(po, []k8sAPI.ServicePort{}))
}
}
if options.Service {
serviceYAML = libpod.GenerateKubeServiceFromV1Pod(podYAML, servicePorts)
}
content, err := generateKubeOutput(podYAML, &serviceYAML, options.Service)
content, err := generateKubeOutput(kubePods, kubeServices, options.Service)
if err != nil {
return nil, err
}
@ -100,23 +105,55 @@ func (ic *ContainerEngine) GenerateKube(ctx context.Context, nameOrIDs []string,
return &entities.GenerateKubeReport{Reader: bytes.NewReader(content)}, nil
}
func generateKubeOutput(podYAML *k8sAPI.Pod, serviceYAML *k8sAPI.Service, hasService bool) ([]byte, error) {
var (
output []byte
marshalledPod []byte
marshalledService []byte
err error
)
func getKubePods(pods []*libpod.Pod, getService bool) ([]*k8sAPI.Pod, []k8sAPI.Service, error) {
kubePods := make([]*k8sAPI.Pod, 0)
kubeServices := make([]k8sAPI.Service, 0)
marshalledPod, err = yaml.Marshal(podYAML)
if err != nil {
return nil, err
for _, p := range pods {
po, svc, err := p.GenerateForKube()
if err != nil {
return nil, nil, err
}
kubePods = append(kubePods, po)
if getService {
kubeServices = append(kubeServices, libpod.GenerateKubeServiceFromV1Pod(po, svc))
}
}
return kubePods, kubeServices, nil
}
func generateKubeOutput(kubePods []*k8sAPI.Pod, kubeServices []k8sAPI.Service, hasService bool) ([]byte, error) {
output := make([]byte, 0)
marshalledPods := make([]byte, 0)
marshalledServices := make([]byte, 0)
for i, p := range kubePods {
if i != 0 {
marshalledPods = append(marshalledPods, []byte("---\n")...)
}
b, err := yaml.Marshal(p)
if err != nil {
return nil, err
}
marshalledPods = append(marshalledPods, b...)
}
if hasService {
marshalledService, err = yaml.Marshal(serviceYAML)
if err != nil {
return nil, err
for i, s := range kubeServices {
if i != 0 {
marshalledServices = append(marshalledServices, []byte("---\n")...)
}
b, err := yaml.Marshal(s)
if err != nil {
return nil, err
}
marshalledServices = append(marshalledServices, b...)
}
}
@ -133,11 +170,12 @@ func generateKubeOutput(podYAML *k8sAPI.Pod, serviceYAML *k8sAPI.Service, hasSer
}
output = append(output, []byte(fmt.Sprintf(header, podmanVersion.Version))...)
output = append(output, marshalledPod...)
// kube generate order is based on helm install order (service, pod...)
if hasService {
output = append(output, marshalledServices...)
output = append(output, []byte("---\n")...)
output = append(output, marshalledService...)
}
output = append(output, marshalledPods...)
return output, nil
}

View File

@ -1,6 +1,7 @@
package abi
import (
"bytes"
"context"
"fmt"
"io"
@ -20,46 +21,79 @@ import (
"github.com/ghodss/yaml"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
yamlv3 "gopkg.in/yaml.v3"
v1apps "k8s.io/api/apps/v1"
v1 "k8s.io/api/core/v1"
)
func (ic *ContainerEngine) PlayKube(ctx context.Context, path string, options entities.PlayKubeOptions) (*entities.PlayKubeReport, error) {
var (
kubeObject v1.ObjectReference
)
report := &entities.PlayKubeReport{}
validKinds := 0
// read yaml document
content, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
if err := yaml.Unmarshal(content, &kubeObject); err != nil {
return nil, errors.Wrapf(err, "unable to read %q as YAML", path)
// split yaml document
documentList, err := splitMultiDocYAML(content)
if err != nil {
return nil, err
}
// NOTE: pkg/bindings/play is also parsing the file.
// A pkg/kube would be nice to refactor and abstract
// parts of the K8s-related code.
switch kubeObject.Kind {
case "Pod":
var podYAML v1.Pod
var podTemplateSpec v1.PodTemplateSpec
if err := yaml.Unmarshal(content, &podYAML); err != nil {
return nil, errors.Wrapf(err, "unable to read YAML %q as Kube Pod", path)
// create pod on each document if it is a pod or deployment
// any other kube kind will be skipped
for _, document := range documentList {
kind, err := getKubeKind(document)
if err != nil {
return nil, errors.Wrapf(err, "unable to read %q as kube YAML", path)
}
podTemplateSpec.ObjectMeta = podYAML.ObjectMeta
podTemplateSpec.Spec = podYAML.Spec
return ic.playKubePod(ctx, podTemplateSpec.ObjectMeta.Name, &podTemplateSpec, options)
case "Deployment":
var deploymentYAML v1apps.Deployment
if err := yaml.Unmarshal(content, &deploymentYAML); err != nil {
return nil, errors.Wrapf(err, "unable to read YAML %q as Kube Deployment", path)
switch kind {
case "Pod":
var podYAML v1.Pod
var podTemplateSpec v1.PodTemplateSpec
if err := yaml.Unmarshal(document, &podYAML); err != nil {
return nil, errors.Wrapf(err, "unable to read YAML %q as Kube Pod", path)
}
podTemplateSpec.ObjectMeta = podYAML.ObjectMeta
podTemplateSpec.Spec = podYAML.Spec
r, err := ic.playKubePod(ctx, podTemplateSpec.ObjectMeta.Name, &podTemplateSpec, options)
if err != nil {
return nil, err
}
report.Pods = append(report.Pods, r.Pods...)
validKinds++
case "Deployment":
var deploymentYAML v1apps.Deployment
if err := yaml.Unmarshal(document, &deploymentYAML); err != nil {
return nil, errors.Wrapf(err, "unable to read YAML %q as Kube Deployment", path)
}
r, err := ic.playKubeDeployment(ctx, &deploymentYAML, options)
if err != nil {
return nil, err
}
report.Pods = append(report.Pods, r.Pods...)
validKinds++
default:
logrus.Infof("kube kind %s not supported", kind)
continue
}
return ic.playKubeDeployment(ctx, &deploymentYAML, options)
default:
return nil, errors.Errorf("invalid YAML kind: %q. [Pod|Deployment] are the only supported Kubernetes Kinds", kubeObject.Kind)
}
if validKinds == 0 {
return nil, fmt.Errorf("YAML document does not contain any supported kube kind")
}
return report, nil
}
func (ic *ContainerEngine) playKubeDeployment(ctx context.Context, deploymentYAML *v1apps.Deployment, options entities.PlayKubeOptions) (*entities.PlayKubeReport, error) {
@ -290,3 +324,45 @@ func readConfigMapFromFile(r io.Reader) (v1.ConfigMap, error) {
return cm, nil
}
// splitMultiDocYAML reads mutiple documents in a YAML file and
// returns them as a list.
func splitMultiDocYAML(yamlContent []byte) ([][]byte, error) {
var documentList [][]byte
d := yamlv3.NewDecoder(bytes.NewReader(yamlContent))
for {
var o interface{}
// read individual document
err := d.Decode(&o)
if err == io.EOF {
break
}
if err != nil {
return nil, errors.Wrapf(err, "multi doc yaml could not be split")
}
if o != nil {
// back to bytes
document, err := yamlv3.Marshal(o)
if err != nil {
return nil, errors.Wrapf(err, "individual doc yaml could not be marshalled")
}
documentList = append(documentList, document)
}
}
return documentList, nil
}
// getKubeKind unmarshals a kube YAML document and returns its kind.
func getKubeKind(obj []byte) (string, error) {
var kubeObject v1.ObjectReference
if err := yaml.Unmarshal(obj, &kubeObject); err != nil {
return "", err
}
return kubeObject.Kind, nil
}

View File

@ -89,3 +89,100 @@ data:
})
}
}
func TestGetKubeKind(t *testing.T) {
tests := []struct {
name string
kubeYAML string
expectError bool
expectedErrorMsg string
expected string
}{
{
"ValidKubeYAML",
`
apiVersion: v1
kind: Pod
`,
false,
"",
"Pod",
},
{
"InvalidKubeYAML",
"InvalidKubeYAML",
true,
"cannot unmarshal",
"",
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
kind, err := getKubeKind([]byte(test.kubeYAML))
if test.expectError {
assert.Error(t, err)
assert.Contains(t, err.Error(), test.expectedErrorMsg)
} else {
assert.NoError(t, err)
assert.Equal(t, test.expected, kind)
}
})
}
}
func TestSplitMultiDocYAML(t *testing.T) {
tests := []struct {
name string
kubeYAML string
expectError bool
expectedErrorMsg string
expected int
}{
{
"ValidNumberOfDocs",
`
apiVersion: v1
kind: Pod
---
apiVersion: v1
kind: Pod
---
apiVersion: v1
kind: Pod
`,
false,
"",
3,
},
{
"InvalidMultiDocYAML",
`
apiVersion: v1
kind: Pod
---
apiVersion: v1
kind: Pod
-
`,
true,
"multi doc yaml could not be split",
0,
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
docs, err := splitMultiDocYAML([]byte(test.kubeYAML))
if test.expectError {
assert.Error(t, err)
assert.Contains(t, err.Error(), test.expectedErrorMsg)
} else {
assert.NoError(t, err)
assert.Equal(t, test.expected, len(docs))
}
})
}
}

View File

@ -155,6 +155,23 @@ var _ = Describe("Podman generate kube", func() {
Expect(numContainers).To(Equal(1))
})
It("podman generate kube multiple pods", func() {
pod1 := podmanTest.Podman([]string{"run", "-dt", "--pod", "new:pod1", ALPINE, "top"})
pod1.WaitWithDefaultTimeout()
Expect(pod1.ExitCode()).To(Equal(0))
pod2 := podmanTest.Podman([]string{"run", "-dt", "--pod", "new:pod2", ALPINE, "top"})
pod2.WaitWithDefaultTimeout()
Expect(pod2.ExitCode()).To(Equal(0))
kube := podmanTest.Podman([]string{"generate", "kube", "pod1", "pod2"})
kube.WaitWithDefaultTimeout()
Expect(kube.ExitCode()).To(Equal(0))
Expect(string(kube.Out.Contents())).To(ContainSubstring(`name: pod1`))
Expect(string(kube.Out.Contents())).To(ContainSubstring(`name: pod2`))
})
It("podman generate kube on pod with host network", func() {
podSession := podmanTest.Podman([]string{"pod", "create", "--name", "testHostNetwork", "--network", "host"})
podSession.WaitWithDefaultTimeout()
@ -537,21 +554,6 @@ var _ = Describe("Podman generate kube", func() {
Expect(inspect.OutputToString()).To(ContainSubstring(`"pid"`))
})
It("podman generate kube multiple pods should fail", func() {
SkipIfRootlessCgroupsV1("Not supported for rootless + CGroupsV1")
pod1 := podmanTest.Podman([]string{"run", "-dt", "--pod", "new:pod1", ALPINE, "top"})
pod1.WaitWithDefaultTimeout()
Expect(pod1.ExitCode()).To(Equal(0))
pod2 := podmanTest.Podman([]string{"run", "-dt", "--pod", "new:pod2", ALPINE, "top"})
pod2.WaitWithDefaultTimeout()
Expect(pod2.ExitCode()).To(Equal(0))
kube := podmanTest.Podman([]string{"generate", "kube", "pod1", "pod2"})
kube.WaitWithDefaultTimeout()
Expect(kube.ExitCode()).ToNot(Equal(0))
})
It("podman generate kube with pods and containers should fail", func() {
pod1 := podmanTest.Podman([]string{"run", "-dt", "--pod", "new:pod1", ALPINE, "top"})
pod1.WaitWithDefaultTimeout()
@ -594,7 +596,7 @@ var _ = Describe("Podman generate kube", func() {
Expect(kube.ExitCode()).To(Equal(0))
})
It("podman generate kube with containers in a pod should fail", func() {
It("podman generate kube with containers in pods should fail", func() {
pod1 := podmanTest.Podman([]string{"run", "-dt", "--pod", "new:pod1", "--name", "top1", ALPINE, "top"})
pod1.WaitWithDefaultTimeout()
Expect(pod1.ExitCode()).To(Equal(0))
@ -603,7 +605,7 @@ var _ = Describe("Podman generate kube", func() {
pod2.WaitWithDefaultTimeout()
Expect(pod2.ExitCode()).To(Equal(0))
kube := podmanTest.Podman([]string{"generate", "kube", "pod1", "pod2"})
kube := podmanTest.Podman([]string{"generate", "kube", "top1", "top2"})
kube.WaitWithDefaultTimeout()
Expect(kube.ExitCode()).ToNot(Equal(0))
})

View File

@ -357,7 +357,8 @@ func writeYaml(content string, fileName string) error {
return nil
}
func generateKubeYaml(kind string, object interface{}, pathname string) error {
// getKubeYaml returns a kubernetes YAML document.
func getKubeYaml(kind string, object interface{}) (string, error) {
var yamlTemplate string
templateBytes := &bytes.Buffer{}
@ -369,19 +370,41 @@ func generateKubeYaml(kind string, object interface{}, pathname string) error {
case "deployment":
yamlTemplate = deploymentYamlTemplate
default:
return fmt.Errorf("unsupported kubernetes kind")
return "", fmt.Errorf("unsupported kubernetes kind")
}
t, err := template.New(kind).Parse(yamlTemplate)
if err != nil {
return err
return "", err
}
if err := t.Execute(templateBytes, object); err != nil {
return "", err
}
return templateBytes.String(), nil
}
// generateKubeYaml writes a kubernetes YAML document.
func generateKubeYaml(kind string, object interface{}, pathname string) error {
k, err := getKubeYaml(kind, object)
if err != nil {
return err
}
return writeYaml(templateBytes.String(), pathname)
return writeYaml(k, pathname)
}
// generateMultiDocKubeYaml writes multiple kube objects in one Yaml document.
func generateMultiDocKubeYaml(kubeObjects []string, pathname string) error {
var multiKube string
for _, k := range kubeObjects {
multiKube += "---\n"
multiKube += k
}
return writeYaml(multiKube, pathname)
}
// ConfigMap describes the options a kube yaml can be configured at configmap level
@ -1698,4 +1721,102 @@ MemoryReservation: {{ .HostConfig.MemoryReservation }}`})
Expect(inspect.ExitCode()).To(Equal(0))
Expect(inspect.OutputToString()).To(Equal("true"))
})
// Multi doc related tests
It("podman play kube multi doc yaml", func() {
yamlDocs := []string{}
podNames := []string{}
serviceTemplate := `apiVersion: v1
kind: Service
metadata:
name: %s
spec:
ports:
- port: 80
protocol: TCP
targetPort: 9376
selector:
app: %s
`
// generate servies, pods and deployments
for i := 0; i < 2; i++ {
podName := fmt.Sprintf("testPod%d", i)
deploymentName := fmt.Sprintf("testDeploy%d", i)
deploymentPodName := fmt.Sprintf("%s-pod-0", deploymentName)
podNames = append(podNames, podName)
podNames = append(podNames, deploymentPodName)
pod := getPod(withPodName(podName))
podDeployment := getPod(withPodName(deploymentName))
deployment := getDeployment(withPod(podDeployment))
deployment.Name = deploymentName
// add services
yamlDocs = append([]string{
fmt.Sprintf(serviceTemplate, podName, podName),
fmt.Sprintf(serviceTemplate, deploymentPodName, deploymentPodName)}, yamlDocs...)
// add pods
k, err := getKubeYaml("pod", pod)
Expect(err).To(BeNil())
yamlDocs = append(yamlDocs, k)
// add deployments
k, err = getKubeYaml("deployment", deployment)
Expect(err).To(BeNil())
yamlDocs = append(yamlDocs, k)
}
// generate multi doc yaml
err = generateMultiDocKubeYaml(yamlDocs, kubeYaml)
Expect(err).To(BeNil())
kube := podmanTest.Podman([]string{"play", "kube", kubeYaml})
kube.WaitWithDefaultTimeout()
Expect(kube.ExitCode()).To(Equal(0))
for _, n := range podNames {
inspect := podmanTest.Podman([]string{"inspect", n, "--format", "'{{ .State }}'"})
inspect.WaitWithDefaultTimeout()
Expect(inspect.ExitCode()).To(Equal(0))
Expect(inspect.OutputToString()).To(ContainSubstring(`Running`))
}
})
It("podman play kube invalid multi doc yaml", func() {
yamlDocs := []string{}
serviceTemplate := `apiVersion: v1
kind: Service
metadata:
name: %s
spec:
ports:
- port: 80
protocol: TCP
targetPort: 9376
selector:
app: %s
---
invalid kube kind
`
// add invalid multi doc yaml
yamlDocs = append(yamlDocs, fmt.Sprintf(serviceTemplate, "foo", "foo"))
// add pod
pod := getPod()
k, err := getKubeYaml("pod", pod)
Expect(err).To(BeNil())
yamlDocs = append(yamlDocs, k)
// generate multi doc yaml
err = generateMultiDocKubeYaml(yamlDocs, kubeYaml)
Expect(err).To(BeNil())
kube := podmanTest.Podman([]string{"play", "kube", kubeYaml})
kube.WaitWithDefaultTimeout()
Expect(kube.ExitCode()).To(Not(Equal(0)))
})
})