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

[WIP] Download kubectl at runtime#1136 #1237

Closed
wants to merge 19 commits into from
Closed
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
2 changes: 1 addition & 1 deletion pkg/cnab/config-adapter/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ func (c *ManifestConverter) generateDependencies() extensions.Dependencies {
Requires: make(map[string]extensions.Dependency, len(c.Manifest.Dependencies)),
}

for name, dep := range c.Manifest.Dependencies {
for name, dep := range c.Manifest.Dependencies.Elements {
r := extensions.Dependency{
Bundle: dep.Tag,
}
Expand Down
25 changes: 15 additions & 10 deletions pkg/cnab/config-adapter/testdata/porter-with-deps.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,21 @@ description: "An example Porter configuration"
tag: getporter/porter-hello

dependencies:
mysql:
tag: "getporter/azure-mysql:5.7"
ad:
tag: "getporter/azure-active-directory"
prereleases: true
storage:
tag: "getporter/azure-blob-storage"
versions:
- 1.x - 2
- 2.1 - 3.x
sequence:
- "ad"
- "storage"
- "mysql"
elements:
mysql:
tag: "getporter/azure-mysql:5.7"
ad:
tag: "getporter/azure-active-directory"
prereleases: true
storage:
tag: "getporter/azure-blob-storage"
versions:
- 1.x - 2
- 2.1 - 3.x
mixins:
- exec

Expand Down
5 changes: 3 additions & 2 deletions pkg/cnab/config-adapter/testdata/porter.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ version: 0.1.0
tag: getporter/porter-hello

dependencies:
mysql:
tag: "getporter/azure-mysql:5.7"
elements:
mysql:
tag: "getporter/azure-mysql:5.7"

mixins:
- exec
Expand Down
20 changes: 20 additions & 0 deletions pkg/cnab/extensions/dependencies.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ var DependenciesExtension = RequiredExtension{
// Dependencies describes the set of custom extension metadata associated with the dependencies spec
// https://github.com/cnabio/cnab-spec/blob/master/500-CNAB-dependencies.md
type Dependencies struct {

// Sequence is a list to order the dependencies
Sequence []string `json:"sequence,omitempty" mapstructure:"sequence"`

// Requires is a list of bundles required by this bundle
Requires map[string]Dependency `json:"requires,omitempty" mapstructure:"requires"`
}
Expand Down Expand Up @@ -62,6 +66,22 @@ func ReadDependencies(bun bundle.Bundle) (Dependencies, error) {
return Dependencies{}, errors.New("unable to read dependencies extension data")
}

// Make sure the Sequence is defined and match the number of deps
if deps.Sequence != nil && len(deps.Sequence) > 0 && len(deps.Sequence) == len(deps.Requires) {
// Copy the original Dependencies
sequencedDeps := &Dependencies{}
sequencedDeps.Sequence = deps.Sequence
sequencedDeps.Requires = make(map[string]Dependency)

// Copy the dependencies according to the desired sequence
for _, seq := range sequencedDeps.Sequence {
// Not sure if we need a deep copy of the dependencies here
sequencedDeps.Requires[seq] = deps.Requires[seq]
}
// Return the sequenced dependencies instead
return sequencedDeps, nil
}
// Return the original dependencies
return deps, nil
}

Expand Down
51 changes: 51 additions & 0 deletions pkg/cnab/extensions/dependencies_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package extensions

import (
"io/ioutil"
"reflect"
"testing"

"github.com/cnabio/cnab-go/bundle"
Expand Down Expand Up @@ -33,3 +34,53 @@ func TestReadDependencyProperties(t *testing.T) {
assert.True(t, dep.Version.AllowPrereleases, "Dependency.Bundle.Version.AllowPrereleases should be true")
assert.Equal(t, []string{"5.7.x"}, dep.Version.Ranges, "Dependency.Bundle.Version.Ranges is incorrect")
}

func TestSortDependenciesBySequence(t *testing.T) {
sequenceMock := []string{"nginx", "storage", "mysql"}

bun := &bundle.Bundle{
Custom: map[string]interface{}{
DependenciesKey: Dependencies{
Sequence: sequenceMock,
Requires: map[string]Dependency{
"mysql": Dependency{
Bundle: "somecloud/mysql",
Version: &DependencyVersion{
AllowPrereleases: true,
Ranges: []string{"5.7.x"},
},
},
"storage": Dependency{
Bundle: "somecloud/blob-storage",
},
"nginx": {
Bundle: "localhost:5000/nginx:1.19",
},
},
},
},
}

deps, err := ReadDependencies(bun)
require.NoError(t, err, "unable to read dependencies extension data")

assert.NotNil(t, deps, "Dependencies was not populated")
assert.Len(t, deps.Requires, 3, "Dependencies.Requires is the wrong length")

dep := deps.Requires["storage"]
assert.NotNil(t, dep, "expected Dependencies.Requires to have an entry for 'storage'")

dep = deps.Requires["mysql"]
assert.NotNil(t, dep, "expected Dependencies.Requires to have an entry for 'mysql'")

dep = deps.Requires["nginx"]
assert.NotNil(t, dep, "expected Dependencies.Requires to have an entry for 'nginx'")

// Get the keys out of the deps.Requires map
keys := reflect.ValueOf(deps.Requires).MapKeys()

// assert the bundles are sorted as sequenced
assert.EqualValues(t, sequenceMock[0], keys[0].Interface().(string))
assert.EqualValues(t, sequenceMock[1], keys[1].Interface().(string))
assert.EqualValues(t, sequenceMock[2], keys[2].Interface().(string))
}
83 changes: 83 additions & 0 deletions pkg/kubernetes/kubernetes.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,14 @@ package kubernetes

import (
"bufio"
"bytes"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"regexp"
"strings"

"get.porter.sh/porter/pkg/context"
Expand Down Expand Up @@ -38,6 +43,25 @@ func NewSchemaBox() *packr.Box {
return packr.New("get.porter.sh/porter/pkg/kubernetes/schema", "./schema")
}

func (m *Mixin) Init() error {
apiServerVersion, err := getKubectlApiServerVersion(m)

if err != nil {
return err
}

if m.KubernetesClientVersion != apiServerVersion {
fmt.Fprintf(m.Out, "Kubectl api server version (%s) does not match client version (%s); downloading a compatible client.\n",
apiServerVersion, m.KubernetesClientVersion)

err := installKubectlClient(m, apiServerVersion)
if err != nil {
return errors.Wrap(err, "unable to install a compatible kubectl client")
}
}
return err
}

func (m *Mixin) getCommandFile(commandFile string, w io.Writer) ([]byte, error) {
if commandFile == "" {
reader := bufio.NewReader(m.In)
Expand Down Expand Up @@ -132,3 +156,62 @@ func (m *Mixin) handleOutputs(outputs []KubernetesOutput) error {
}
return nil
}

func installKubectlClient(m *Mixin, version string) error {

url := fmt.Sprintf("https://storage.googleapis.com/kubernetes-release/release/%s/bin/linux/amd64/kubectl", version)

// Fetch archive from url
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return errors.Wrap(err, "failed to construct GET request for fetching kubectl client binary")
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return errors.Wrapf(err, "failed to download kubectl client binary via url: %s", url)
}
defer res.Body.Close()

// Create a temp dir
tmpDir, err := m.FileSystem.TempDir("", "tmp")
if err != nil {
return errors.Wrap(err, "unable to create a temporary directory for downloading the kubectl client binary")
}
defer os.RemoveAll(tmpDir)

// Create the local archive
kubectlBinPath, err := m.FileSystem.Create(filepath.Join(tmpDir, "kubectl"))
if err != nil {
return errors.Wrap(err, "unable to create a local file for the kubectl client binary")
}

// Copy response body to local archive
_, err = io.Copy(kubectlBinPath, res.Body)
if err != nil {
return errors.Wrap(err, "unable to copy the kubectl client binary to the local archive file")
}

// Move the kubectl binary into the appropriate location
binPath := "/usr/local/bin/kubectl"
err = m.FileSystem.Rename(fmt.Sprintf("%s", kubectlBinPath), binPath)
if err != nil {
return errors.Wrapf(err, "unable to install the kubectl client binary to %q", binPath)
}
return nil
}

func getKubectlApiServerVersion(m *Mixin) (string, error) {
var stderr bytes.Buffer

cmd := m.NewCommand("kubectl", "api-versions")
cmd.Stderr = &stderr

outputBytes, err := cmd.Output()
if err != nil {
return "", errors.Wrapf(err, "unable to determine kubernetes api server version: %s", stderr.String())
}
re := regexp.MustCompile(`v[0-9]*\.[0-9]*\.[0-9]*`)
version := re.FindString(string(outputBytes))

return version, nil
}
16 changes: 10 additions & 6 deletions pkg/manifest/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ func (m *Manifest) Validate() error {
}
}

for _, dep := range m.Dependencies {
for _, dep := range m.Dependencies.Elements {
err = dep.Validate()
if err != nil {
result = multierror.Append(result, err)
Expand Down Expand Up @@ -498,12 +498,16 @@ func (mi *MappedImage) Validate() error {
return nil
}

type Dependency struct {
Tag string `yaml:"tag"`
Versions []string `yaml:"versions"`
AllowPrereleases bool `yaml:"prereleases"`
type DependenciesDefinition struct {
Sequence []string `yaml:"sequence,omitempty"`
Elements map[string]Dependency `yaml:"elements,omitempty"`
}

Parameters map[string]string `yaml:"parameters,omitempty"`
type Dependency struct {
Tag string `yaml:"tag"`
Versions []string `yaml:"versions"`
AllowPrereleases bool `yaml:"prereleases"`
Parameters map[string]string `yaml:"parameters,omitempty"`
}

func (d *Dependency) Validate() error {
Expand Down
4 changes: 2 additions & 2 deletions pkg/porter/dependencies.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,11 +203,11 @@ func (e *dependencyExecutioner) prepareDependency(dep *queuedDependency) error {
// DEP:
// parameters:
// PARAM: VALUE
if depDef, ok := e.Manifest.Dependencies[dep.Alias]; ok {
if depDef, ok := e.Manifest.Dependencies.Elements[dep.Alias]; ok {
for paramName, value := range depDef.Parameters {
// Make sure the parameter is defined in the bundle
if _, ok := depParams[paramName]; !ok {
return errors.Errorf("invalid dependencies.%s.parameters entry, %s is not a parameter defined in that bundle", dep.Alias, paramName)
return errors.Errorf("invalid dependencies.elements.%s.parameters entry, %s is not a parameter defined in that bundle", dep.Alias, paramName)
}

if dep.Parameters == nil {
Expand Down
4 changes: 2 additions & 2 deletions pkg/runtime/runtime-manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@ func (m *RuntimeManifest) loadBundle() error {
}

func (m *RuntimeManifest) loadDependencyDefinitions() error {
m.bundles = make(map[string]bundle.Bundle, len(m.Dependencies))
for alias := range m.Dependencies {
m.bundles = make(map[string]bundle.Bundle, len(m.Dependencies.Elements))
for alias := range m.Dependencies.Elements {
bunD, err := GetDependencyDefinition(m.Context, alias)
if err != nil {
return err
Expand Down