mirror of https://github.com/portainer/portainer
import libhelm into portainer (#8128)
parent
241440a474
commit
d2f6d1e415
|
@ -9,7 +9,6 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/portainer/libhelm"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/apikey"
|
||||
"github.com/portainer/portainer/api/build"
|
||||
|
@ -40,6 +39,7 @@ import (
|
|||
kubecli "github.com/portainer/portainer/api/kubernetes/cli"
|
||||
"github.com/portainer/portainer/api/ldap"
|
||||
"github.com/portainer/portainer/api/oauth"
|
||||
"github.com/portainer/portainer/api/pkg/libhelm"
|
||||
"github.com/portainer/portainer/api/scheduler"
|
||||
"github.com/portainer/portainer/api/stacks/deployments"
|
||||
|
||||
|
|
|
@ -4,7 +4,6 @@ import (
|
|||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/portainer/libhelm"
|
||||
"github.com/portainer/libhelm/options"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
@ -12,6 +11,7 @@ import (
|
|||
"github.com/portainer/portainer/api/http/middlewares"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/kubernetes"
|
||||
"github.com/portainer/portainer/api/pkg/libhelm"
|
||||
)
|
||||
|
||||
type requestBouncer interface {
|
||||
|
|
|
@ -6,7 +6,7 @@ import (
|
|||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/portainer/libhelm"
|
||||
"github.com/portainer/portainer/api/pkg/libhelm"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
|
|
|
@ -7,13 +7,13 @@ import (
|
|||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/portainer/libhelm"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
"github.com/portainer/portainer/api/internal/edge"
|
||||
"github.com/portainer/portainer/api/pkg/libhelm"
|
||||
)
|
||||
|
||||
type settingsUpdatePayload struct {
|
||||
|
|
|
@ -7,7 +7,6 @@ import (
|
|||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/portainer/libhelm"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/adminmonitor"
|
||||
"github.com/portainer/portainer/api/apikey"
|
||||
|
@ -60,6 +59,7 @@ import (
|
|||
"github.com/portainer/portainer/api/internal/ssl"
|
||||
k8s "github.com/portainer/portainer/api/kubernetes"
|
||||
"github.com/portainer/portainer/api/kubernetes/cli"
|
||||
"github.com/portainer/portainer/api/pkg/libhelm"
|
||||
"github.com/portainer/portainer/api/scheduler"
|
||||
"github.com/portainer/portainer/api/stacks/deployments"
|
||||
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
Copyright 2021 Portainer.io
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@ -0,0 +1,17 @@
|
|||
# LibHelm
|
||||
|
||||
A helm abstraction for Portainer.
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
go get github.com/portainer/portainer/api/pkg/libhelm
|
||||
```
|
||||
|
||||
## Tests
|
||||
|
||||
### Integration
|
||||
|
||||
```sh
|
||||
INTEGRATION_TEST=1 go test binary/*.go
|
||||
```
|
|
@ -0,0 +1,29 @@
|
|||
package binary
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
"github.com/portainer/libhelm/options"
|
||||
)
|
||||
|
||||
// Get runs `helm get` with specified get options.
|
||||
// The get options translate to CLI arguments which are passed in to the helm binary when executing install.
|
||||
func (hbpm *helmBinaryPackageManager) Get(getOpts options.GetOptions) ([]byte, error) {
|
||||
if getOpts.Name == "" || getOpts.ReleaseResource == "" {
|
||||
return nil, errors.New("release name and release resource are required")
|
||||
}
|
||||
|
||||
args := []string{
|
||||
string(getOpts.ReleaseResource),
|
||||
getOpts.Name,
|
||||
}
|
||||
if getOpts.Namespace != "" {
|
||||
args = append(args, "--namespace", getOpts.Namespace)
|
||||
}
|
||||
|
||||
result, err := hbpm.runWithKubeConfig("get", args, getOpts.KubernetesClusterAccess)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to run helm get on specified args")
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
package binary
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os/exec"
|
||||
"path"
|
||||
"runtime"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/portainer/libhelm/options"
|
||||
)
|
||||
|
||||
// helmBinaryPackageManager is a wrapper for the helm binary which implements HelmPackageManager
|
||||
type helmBinaryPackageManager struct {
|
||||
binaryPath string
|
||||
}
|
||||
|
||||
// NewHelmBinaryPackageManager initializes a new HelmPackageManager service.
|
||||
func NewHelmBinaryPackageManager(binaryPath string) *helmBinaryPackageManager {
|
||||
return &helmBinaryPackageManager{binaryPath: binaryPath}
|
||||
}
|
||||
|
||||
// runWithKubeConfig will execute run against the provided Kubernetes cluster with kubeconfig as cli arguments.
|
||||
func (hbpm *helmBinaryPackageManager) runWithKubeConfig(command string, args []string, kca *options.KubernetesClusterAccess) ([]byte, error) {
|
||||
cmdArgs := make([]string, 0)
|
||||
if kca != nil {
|
||||
cmdArgs = append(cmdArgs, "--kube-apiserver", kca.ClusterServerURL)
|
||||
cmdArgs = append(cmdArgs, "--kube-token", kca.AuthToken)
|
||||
cmdArgs = append(cmdArgs, "--kube-ca-file", kca.CertificateAuthorityFile)
|
||||
}
|
||||
cmdArgs = append(cmdArgs, args...)
|
||||
return hbpm.run(command, cmdArgs)
|
||||
}
|
||||
|
||||
// run will execute helm command against the provided Kubernetes cluster.
|
||||
// The endpointId and authToken are dynamic params (based on the user) that allow helm to execute commands
|
||||
// in the context of the current user against specified k8s cluster.
|
||||
func (hbpm *helmBinaryPackageManager) run(command string, args []string) ([]byte, error) {
|
||||
cmdArgs := make([]string, 0)
|
||||
cmdArgs = append(cmdArgs, command)
|
||||
cmdArgs = append(cmdArgs, args...)
|
||||
|
||||
helmPath := path.Join(hbpm.binaryPath, "helm")
|
||||
if runtime.GOOS == "windows" {
|
||||
helmPath = path.Join(hbpm.binaryPath, "helm.exe")
|
||||
}
|
||||
|
||||
var stderr bytes.Buffer
|
||||
cmd := exec.Command(helmPath, cmdArgs...)
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, stderr.String())
|
||||
}
|
||||
|
||||
return output, nil
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
package binary
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/portainer/libhelm/options"
|
||||
"github.com/portainer/libhelm/release"
|
||||
)
|
||||
|
||||
// Install runs `helm install` with specified install options.
|
||||
// The install options translate to CLI arguments which are passed in to the helm binary when executing install.
|
||||
func (hbpm *helmBinaryPackageManager) Install(installOpts options.InstallOptions) (*release.Release, error) {
|
||||
if installOpts.Name == "" {
|
||||
installOpts.Name = "--generate-name"
|
||||
}
|
||||
args := []string{
|
||||
installOpts.Name,
|
||||
installOpts.Chart,
|
||||
"--repo", installOpts.Repo,
|
||||
"--output", "json",
|
||||
}
|
||||
if installOpts.Namespace != "" {
|
||||
args = append(args, "--namespace", installOpts.Namespace)
|
||||
}
|
||||
if installOpts.ValuesFile != "" {
|
||||
args = append(args, "--values", installOpts.ValuesFile)
|
||||
}
|
||||
if installOpts.Wait {
|
||||
args = append(args, "--wait")
|
||||
}
|
||||
if installOpts.PostRenderer != "" {
|
||||
args = append(args, "--post-renderer", installOpts.PostRenderer)
|
||||
}
|
||||
|
||||
result, err := hbpm.runWithKubeConfig("install", args, installOpts.KubernetesClusterAccess)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to run helm install on specified args")
|
||||
}
|
||||
|
||||
response := &release.Release{}
|
||||
err = json.Unmarshal(result, &response)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to unmarshal helm install response to Release struct")
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
package binary
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/portainer/libhelm/options"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func createValuesFile(values string) (string, error) {
|
||||
file, err := os.CreateTemp("", "helm-values")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
_, err = file.WriteString(values)
|
||||
if err != nil {
|
||||
file.Close()
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = file.Close()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return file.Name(), nil
|
||||
}
|
||||
|
||||
// getHelmBinaryPath is helper function to get local helm binary path (if helm is in path)
|
||||
func getHelmBinaryPath() (string, error) {
|
||||
path, err := exec.LookPath("helm")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
dir, err := filepath.Abs(filepath.Dir(path))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return dir, nil
|
||||
}
|
||||
|
||||
func Test_Install(t *testing.T) {
|
||||
ensureIntegrationTest(t)
|
||||
is := assert.New(t)
|
||||
|
||||
path, err := getHelmBinaryPath()
|
||||
is.NoError(err, "helm binary must exist in path to run tests")
|
||||
|
||||
hbpm := NewHelmBinaryPackageManager(path)
|
||||
|
||||
t.Run("successfully installs nginx chart with name test-nginx", func(t *testing.T) {
|
||||
// helm install test-nginx --repo https://charts.bitnami.com/bitnami nginx
|
||||
installOpts := options.InstallOptions{
|
||||
Name: "test-nginx",
|
||||
Chart: "nginx",
|
||||
Repo: "https://charts.bitnami.com/bitnami",
|
||||
}
|
||||
|
||||
release, err := hbpm.Install(installOpts)
|
||||
defer hbpm.run("uninstall", []string{"test-nginx"})
|
||||
|
||||
is.NoError(err, "should successfully install release", release)
|
||||
})
|
||||
|
||||
t.Run("successfully installs nginx chart with generated name", func(t *testing.T) {
|
||||
// helm install --generate-name --repo https://charts.bitnami.com/bitnami nginx
|
||||
installOpts := options.InstallOptions{
|
||||
Chart: "nginx",
|
||||
Repo: "https://charts.bitnami.com/bitnami",
|
||||
}
|
||||
release, err := hbpm.Install(installOpts)
|
||||
defer hbpm.run("uninstall", []string{release.Name})
|
||||
|
||||
is.NoError(err, "should successfully install release", release)
|
||||
})
|
||||
|
||||
t.Run("successfully installs nginx with values", func(t *testing.T) {
|
||||
// helm install test-nginx-2 --repo https://charts.bitnami.com/bitnami nginx --values /tmp/helm-values3161785816
|
||||
values, err := createValuesFile("service:\n port: 8081")
|
||||
is.NoError(err, "should create a values file")
|
||||
|
||||
defer os.Remove(values)
|
||||
|
||||
installOpts := options.InstallOptions{
|
||||
Name: "test-nginx-2",
|
||||
Chart: "nginx",
|
||||
Repo: "https://charts.bitnami.com/bitnami",
|
||||
ValuesFile: values,
|
||||
}
|
||||
release, err := hbpm.Install(installOpts)
|
||||
defer hbpm.run("uninstall", []string{"test-nginx-2"})
|
||||
|
||||
is.NoError(err, "should successfully install release", release)
|
||||
})
|
||||
|
||||
t.Run("successfully installs portainer chart with name portainer-test", func(t *testing.T) {
|
||||
// helm install portainer-test portainer --repo https://portainer.github.io/k8s/
|
||||
installOpts := options.InstallOptions{
|
||||
Name: "portainer-test",
|
||||
Chart: "portainer",
|
||||
Repo: "https://portainer.github.io/k8s/",
|
||||
}
|
||||
release, err := hbpm.Install(installOpts)
|
||||
defer hbpm.run("uninstall", []string{installOpts.Name})
|
||||
|
||||
is.NoError(err, "should successfully install release", release)
|
||||
})
|
||||
}
|
||||
|
||||
func ensureIntegrationTest(t *testing.T) {
|
||||
if _, ok := os.LookupEnv("INTEGRATION_TEST"); !ok {
|
||||
t.Skip("skip an integration test")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
package binary
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/portainer/libhelm/options"
|
||||
"github.com/portainer/libhelm/release"
|
||||
)
|
||||
|
||||
// List runs `helm list --output json --filter <filter> --selector <selector> --namespace <namespace>` with specified list options.
|
||||
// The list options translate to CLI args the helm binary
|
||||
func (hbpm *helmBinaryPackageManager) List(listOpts options.ListOptions) ([]release.ReleaseElement, error) {
|
||||
args := []string{"--output", "json"}
|
||||
|
||||
if listOpts.Filter != "" {
|
||||
args = append(args, "--filter", listOpts.Filter)
|
||||
}
|
||||
if listOpts.Selector != "" {
|
||||
args = append(args, "--selector", listOpts.Selector)
|
||||
}
|
||||
if listOpts.Namespace != "" {
|
||||
args = append(args, "--namespace", listOpts.Namespace)
|
||||
}
|
||||
|
||||
result, err := hbpm.runWithKubeConfig("list", args, listOpts.KubernetesClusterAccess)
|
||||
if err != nil {
|
||||
return []release.ReleaseElement{}, errors.Wrap(err, "failed to run helm list on specified args")
|
||||
}
|
||||
|
||||
response := []release.ReleaseElement{}
|
||||
err = json.Unmarshal(result, &response)
|
||||
if err != nil {
|
||||
return []release.ReleaseElement{}, errors.Wrap(err, "failed to unmarshal helm list response to releastElement list")
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
package binary
|
||||
|
||||
// Package common implements common functionality for the helm.
|
||||
// The functionality does not rely on the implementation of `HelmPackageManager`
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/portainer/libhelm/options"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var errRequiredSearchOptions = errors.New("repo is required")
|
||||
|
||||
type File struct {
|
||||
APIVersion string `yaml:"apiVersion" json:"apiVersion"`
|
||||
Entries map[string][]Entry `yaml:"entries" json:"entries"`
|
||||
Generated string `yaml:"generated" json:"generated"`
|
||||
}
|
||||
|
||||
type Annotations struct {
|
||||
Category string `yaml:"category" json:"category"`
|
||||
}
|
||||
|
||||
type Entry struct {
|
||||
Annotations *Annotations `yaml:"annotations" json:"annotations,omitempty"`
|
||||
Created string `yaml:"created" json:"created"`
|
||||
Deprecated bool `yaml:"deprecated" json:"deprecated"`
|
||||
Description string `yaml:"description" json:"description"`
|
||||
Digest string `yaml:"digest" json:"digest"`
|
||||
Home string `yaml:"home" json:"home"`
|
||||
Name string `yaml:"name" json:"name"`
|
||||
Sources []string `yaml:"sources" json:"sources"`
|
||||
Urls []string `yaml:"urls" json:"urls"`
|
||||
Version string `yaml:"version" json:"version"`
|
||||
Icon string `yaml:"icon" json:"icon,omitempty"`
|
||||
}
|
||||
|
||||
// SearchRepo downloads the `index.yaml` file for specified repo, parses it and returns JSON to caller.
|
||||
// The functionality is similar to that of what `helm search repo [chart] --repo <repo>` CLI runs;
|
||||
// this approach is used instead since the `helm search repo` requires a repo to be added to the global helm cache
|
||||
func (hbpm *helmBinaryPackageManager) SearchRepo(searchRepoOpts options.SearchRepoOptions) ([]byte, error) {
|
||||
if searchRepoOpts.Repo == "" {
|
||||
return nil, errRequiredSearchOptions
|
||||
}
|
||||
|
||||
// The current index.yaml is ~9MB on bitnami.
|
||||
// At a slow @2mbit download = 40s. @100bit = ~1s.
|
||||
// I'm seeing 3 - 4s over wifi.
|
||||
// Give ample time but timeout for now. Can be improved in the future
|
||||
client := http.Client{
|
||||
Timeout: 60 * time.Second,
|
||||
}
|
||||
|
||||
url, err := url.ParseRequestURI(searchRepoOpts.Repo)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, fmt.Sprintf("invalid helm chart URL: %s", searchRepoOpts.Repo))
|
||||
}
|
||||
|
||||
url.Path = path.Join(url.Path, "index.yaml")
|
||||
resp, err := client.Get(url.String())
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get index file")
|
||||
}
|
||||
|
||||
var file File
|
||||
err = yaml.NewDecoder(resp.Body).Decode(&file)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to decode index file")
|
||||
}
|
||||
|
||||
result, err := json.Marshal(file)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to marshal index file")
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
package binary
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/portainer/libhelm/libhelmtest"
|
||||
"github.com/portainer/libhelm/options"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_SearchRepo(t *testing.T) {
|
||||
libhelmtest.EnsureIntegrationTest(t)
|
||||
is := assert.New(t)
|
||||
|
||||
hpm := NewHelmBinaryPackageManager("")
|
||||
|
||||
type testCase struct {
|
||||
name string
|
||||
url string
|
||||
invalid bool
|
||||
}
|
||||
|
||||
tests := []testCase{
|
||||
{"not a helm repo", "https://portainer.io", true},
|
||||
{"bitnami helm repo", "https://charts.bitnami.com/bitnami", false},
|
||||
{"portainer helm repo", "https://portainer.github.io/k8s/", false},
|
||||
{"gitlap helm repo with trailing slash", "https://charts.gitlab.io/", false},
|
||||
{"elastic helm repo with trailing slash", "https://helm.elastic.co/", false},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
func(tc testCase) {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
response, err := hpm.SearchRepo(options.SearchRepoOptions{Repo: tc.url})
|
||||
if tc.invalid {
|
||||
is.Errorf(err, "error expected: %s", tc.url)
|
||||
} else {
|
||||
is.NoError(err, "no error expected: %s", tc.url)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
is.NotEmpty(response, "response expected")
|
||||
}
|
||||
})
|
||||
}(test)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package binary
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
"github.com/portainer/libhelm/options"
|
||||
)
|
||||
|
||||
var errRequiredShowOptions = errors.New("chart, repo and output format are required")
|
||||
|
||||
// Show runs `helm show <command> <chart> --repo <repo>` with specified show options.
|
||||
// The show options translate to CLI arguments which are passed in to the helm binary when executing install.
|
||||
func (hbpm *helmBinaryPackageManager) Show(showOpts options.ShowOptions) ([]byte, error) {
|
||||
if showOpts.Chart == "" || showOpts.Repo == "" || showOpts.OutputFormat == "" {
|
||||
return nil, errRequiredShowOptions
|
||||
}
|
||||
|
||||
args := []string{
|
||||
string(showOpts.OutputFormat),
|
||||
showOpts.Chart,
|
||||
"--repo", showOpts.Repo,
|
||||
}
|
||||
|
||||
result, err := hbpm.run("show", args)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to run helm show on specified args")
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
|
@ -0,0 +1,160 @@
|
|||
package test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/portainer/libhelm/options"
|
||||
"github.com/portainer/libhelm/release"
|
||||
"github.com/portainer/portainer/api/pkg/libhelm"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const (
|
||||
MockDataIndex = "mock-index"
|
||||
MockDataChart = "mock-chart"
|
||||
MockDataReadme = "mock-readme"
|
||||
MockDataValues = "mock-values"
|
||||
)
|
||||
|
||||
const (
|
||||
MockReleaseHooks = "mock-release-hooks"
|
||||
MockReleaseManifest = "mock-release-manifest"
|
||||
MockReleaseNotes = "mock-release-notes"
|
||||
MockReleaseValues = "mock-release-values"
|
||||
)
|
||||
|
||||
// helmMockPackageManager is a test package for helm related http handler testing
|
||||
// Note: this package currently uses a slice in a way that is not thread safe.
|
||||
// Do not use this package for concurrent tests.
|
||||
type helmMockPackageManager struct{}
|
||||
|
||||
// NewMockHelmBinaryPackageManager initializes a new HelmPackageManager service (a mock instance)
|
||||
func NewMockHelmBinaryPackageManager(binaryPath string) libhelm.HelmPackageManager {
|
||||
return &helmMockPackageManager{}
|
||||
}
|
||||
|
||||
var mockCharts = []release.ReleaseElement{}
|
||||
|
||||
func newMockReleaseElement(installOpts options.InstallOptions) *release.ReleaseElement {
|
||||
return &release.ReleaseElement{
|
||||
Name: installOpts.Name,
|
||||
Namespace: installOpts.Namespace,
|
||||
Updated: "date/time",
|
||||
Status: "deployed",
|
||||
Chart: installOpts.Chart,
|
||||
AppVersion: "1.2.3",
|
||||
}
|
||||
}
|
||||
|
||||
func newMockRelease(re *release.ReleaseElement) *release.Release {
|
||||
return &release.Release{
|
||||
Name: re.Name,
|
||||
Namespace: re.Namespace,
|
||||
}
|
||||
}
|
||||
|
||||
// Install a helm chart (not thread safe)
|
||||
func (hpm *helmMockPackageManager) Install(installOpts options.InstallOptions) (*release.Release, error) {
|
||||
|
||||
releaseElement := newMockReleaseElement(installOpts)
|
||||
|
||||
// Enforce only one chart with the same name per namespace
|
||||
for i, rel := range mockCharts {
|
||||
if rel.Name == installOpts.Name && rel.Namespace == installOpts.Namespace {
|
||||
mockCharts[i] = *releaseElement
|
||||
return newMockRelease(releaseElement), nil
|
||||
}
|
||||
}
|
||||
|
||||
mockCharts = append(mockCharts, *releaseElement)
|
||||
return newMockRelease(releaseElement), nil
|
||||
}
|
||||
|
||||
// Show values/readme/chart etc
|
||||
func (hpm *helmMockPackageManager) Show(showOpts options.ShowOptions) ([]byte, error) {
|
||||
switch showOpts.OutputFormat {
|
||||
case options.ShowChart:
|
||||
return []byte(MockDataChart), nil
|
||||
case options.ShowReadme:
|
||||
return []byte(MockDataReadme), nil
|
||||
case options.ShowValues:
|
||||
return []byte(MockDataValues), nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Get release details - all, hooks, manifest, notes and values
|
||||
func (hpm *helmMockPackageManager) Get(getOpts options.GetOptions) ([]byte, error) {
|
||||
switch getOpts.ReleaseResource {
|
||||
case options.GetAll:
|
||||
return []byte(strings.Join([]string{MockReleaseHooks, MockReleaseManifest, MockReleaseNotes, MockReleaseValues}, "---\n")), nil
|
||||
case options.GetHooks:
|
||||
return []byte(MockReleaseHooks), nil
|
||||
case options.GetManifest:
|
||||
return []byte(MockReleaseManifest), nil
|
||||
case options.GetNotes:
|
||||
return []byte(MockReleaseNotes), nil
|
||||
case options.GetValues:
|
||||
return []byte(MockReleaseValues), nil
|
||||
default:
|
||||
return nil, errors.New("invalid release resource")
|
||||
}
|
||||
}
|
||||
|
||||
// Uninstall a helm chart (not thread safe)
|
||||
func (hpm *helmMockPackageManager) Uninstall(uninstallOpts options.UninstallOptions) error {
|
||||
for i, rel := range mockCharts {
|
||||
if rel.Name == uninstallOpts.Name && rel.Namespace == uninstallOpts.Namespace {
|
||||
mockCharts = append(mockCharts[:i], mockCharts[i+1:]...)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// List a helm chart (not thread safe)
|
||||
func (hpm *helmMockPackageManager) List(listOpts options.ListOptions) ([]release.ReleaseElement, error) {
|
||||
return mockCharts, nil
|
||||
}
|
||||
|
||||
const mockPortainerIndex = `apiVersion: v1
|
||||
entries:
|
||||
portainer:
|
||||
- apiVersion: v2
|
||||
appVersion: 2.0.0
|
||||
created: "2020-12-01T21:51:37.367634957Z"
|
||||
description: Helm chart used to deploy the Portainer for Kubernetes
|
||||
digest: f0e13dd3e7a05d17cb35c7879ffa623fd43b2c10ca968203e302b7a6c2764ddb
|
||||
home: https://www.portainer.io
|
||||
icon: https://github.com/portainer/portainer/raw/develop/app/assets/ico/apple-touch-icon.png
|
||||
maintainers:
|
||||
- email: davidy@funkypenguin.co.nz
|
||||
name: funkypenguin
|
||||
url: https://www.funkypenguin.co.nz
|
||||
name: portainer
|
||||
sources:
|
||||
- https://github.com/portainer/k8s
|
||||
type: application
|
||||
urls:
|
||||
- https://github.com/portainer/k8s/releases/download/portainer-1.0.6/portainer-1.0.6.tgz
|
||||
version: 1.0.6
|
||||
generated: "2020-08-19T00:00:46.754739363Z"`
|
||||
|
||||
func (hbpm *helmMockPackageManager) SearchRepo(searchRepoOpts options.SearchRepoOptions) ([]byte, error) {
|
||||
// Always return the same repo data no matter what
|
||||
reader := strings.NewReader(mockPortainerIndex)
|
||||
|
||||
var file release.File
|
||||
err := yaml.NewDecoder(reader).Decode(&file)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to decode index file")
|
||||
}
|
||||
|
||||
result, err := json.Marshal(file)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to marshal index file")
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package binary
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
"github.com/portainer/libhelm/options"
|
||||
)
|
||||
|
||||
var errRequiredUninstallOptions = errors.New("release name is required")
|
||||
|
||||
// Uninstall runs `helm uninstall <name> --namespace <namespace>` with specified uninstall options.
|
||||
// The uninstall options translate to CLI arguments which are passed in to the helm binary when executing uninstall.
|
||||
func (hbpm *helmBinaryPackageManager) Uninstall(uninstallOpts options.UninstallOptions) error {
|
||||
if uninstallOpts.Name == "" {
|
||||
return errRequiredUninstallOptions
|
||||
}
|
||||
|
||||
args := []string{uninstallOpts.Name}
|
||||
|
||||
if uninstallOpts.Namespace != "" {
|
||||
args = append(args, "--namespace", uninstallOpts.Namespace)
|
||||
}
|
||||
|
||||
_, err := hbpm.runWithKubeConfig("uninstall", args, uninstallOpts.KubernetesClusterAccess)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to run helm uninstall on specified args")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package libhelm
|
||||
|
||||
import (
|
||||
"github.com/portainer/libhelm/options"
|
||||
"github.com/portainer/libhelm/release"
|
||||
)
|
||||
|
||||
// HelmPackageManager represents a service that interfaces with Helm
|
||||
type HelmPackageManager interface {
|
||||
Show(showOpts options.ShowOptions) ([]byte, error)
|
||||
SearchRepo(searchRepoOpts options.SearchRepoOptions) ([]byte, error)
|
||||
Get(getOpts options.GetOptions) ([]byte, error)
|
||||
List(listOpts options.ListOptions) ([]release.ReleaseElement, error)
|
||||
Install(installOpts options.InstallOptions) (*release.Release, error)
|
||||
Uninstall(uninstallOpts options.UninstallOptions) error
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package libhelmtest
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// EnsureIntegrationTest disables live integration tests that prevent portainer CI checks from succeeding on github
|
||||
func EnsureIntegrationTest(t *testing.T) {
|
||||
if _, ok := os.LookupEnv("INTEGRATION_TEST"); !ok {
|
||||
t.Skip("skip an integration test")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package libhelm
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/portainer/libhelm/binary"
|
||||
)
|
||||
|
||||
// HelmConfig is a struct that holds the configuration for the Helm package manager
|
||||
type HelmConfig struct {
|
||||
BinaryPath string `example:"/portainer/dist"`
|
||||
}
|
||||
|
||||
var errBinaryPathNotSpecified = errors.New("binary path not specified")
|
||||
|
||||
// NewHelmPackageManager returns a new instance of HelmPackageManager based on HelmConfig
|
||||
func NewHelmPackageManager(config HelmConfig) (HelmPackageManager, error) {
|
||||
if config.BinaryPath != "" {
|
||||
return binary.NewHelmBinaryPackageManager(config.BinaryPath), nil
|
||||
}
|
||||
return nil, errBinaryPathNotSpecified
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package options
|
||||
|
||||
// KubernetesClusterAccess represents core details which can be used to generate KubeConfig file/data
|
||||
type KubernetesClusterAccess struct {
|
||||
ClusterServerURL string `example:"https://mycompany.k8s.com"`
|
||||
CertificateAuthorityFile string `example:"/data/tls/localhost.crt"`
|
||||
AuthToken string `example:"ey..."`
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package options
|
||||
|
||||
// releaseResource are the supported `helm get` sub-commands
|
||||
// to see all available sub-commands run `helm get --help`
|
||||
type releaseResource string
|
||||
|
||||
const (
|
||||
GetAll releaseResource = "all"
|
||||
GetHooks releaseResource = "hooks"
|
||||
GetManifest releaseResource = "manifest"
|
||||
GetNotes releaseResource = "notes"
|
||||
GetValues releaseResource = "values"
|
||||
)
|
||||
|
||||
type GetOptions struct {
|
||||
Name string
|
||||
Namespace string
|
||||
ReleaseResource releaseResource
|
||||
KubernetesClusterAccess *KubernetesClusterAccess
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package options
|
||||
|
||||
type InstallOptions struct {
|
||||
Name string
|
||||
Chart string
|
||||
Namespace string
|
||||
Repo string
|
||||
Wait bool
|
||||
ValuesFile string
|
||||
PostRenderer string
|
||||
KubernetesClusterAccess *KubernetesClusterAccess
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package options
|
||||
|
||||
// ListOptions are portainer supported options for `helm list`
|
||||
type ListOptions struct {
|
||||
Filter string
|
||||
Selector string
|
||||
Namespace string
|
||||
KubernetesClusterAccess *KubernetesClusterAccess
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package options
|
||||
|
||||
type SearchRepoOptions struct {
|
||||
Repo string
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package options
|
||||
|
||||
// ShowOutputFormat is the format of the output of `helm show`
|
||||
type ShowOutputFormat string
|
||||
|
||||
const (
|
||||
// ShowAll is the format which shows all the information of a chart
|
||||
ShowAll ShowOutputFormat = "all"
|
||||
// ShowChart is the format which only shows the chart's definition
|
||||
ShowChart ShowOutputFormat = "chart"
|
||||
// ShowValues is the format which only shows the chart's values
|
||||
ShowValues ShowOutputFormat = "values"
|
||||
// ShowReadme is the format which only shows the chart's README
|
||||
ShowReadme ShowOutputFormat = "readme"
|
||||
)
|
||||
|
||||
// ShowOptions are portainer supported options for `helm install`
|
||||
type ShowOptions struct {
|
||||
OutputFormat ShowOutputFormat
|
||||
Chart string
|
||||
Repo string
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package options
|
||||
|
||||
// UninstallOptions are portainer supported options for `helm uninstall`
|
||||
type UninstallOptions struct {
|
||||
Name string
|
||||
Namespace string
|
||||
KubernetesClusterAccess *KubernetesClusterAccess
|
||||
}
|
|
@ -0,0 +1,230 @@
|
|||
package release
|
||||
|
||||
import "github.com/portainer/libhelm/time"
|
||||
|
||||
// Release is the struct that holds the information for a helm release.
|
||||
// The struct definitions have been copied from the offical Helm Golang client/library.
|
||||
|
||||
// ReleaseElement is a struct that represents a release
|
||||
// This is the official struct from the helm project (golang codebase) - exported
|
||||
type ReleaseElement struct {
|
||||
Name string `json:"name"`
|
||||
Namespace string `json:"namespace"`
|
||||
Revision string `json:"revision"`
|
||||
Updated string `json:"updated"`
|
||||
Status string `json:"status"`
|
||||
Chart string `json:"chart"`
|
||||
AppVersion string `json:"app_version"`
|
||||
}
|
||||
|
||||
// Release describes a deployment of a chart, together with the chart
|
||||
// and the variables used to deploy that chart.
|
||||
type Release struct {
|
||||
// Name is the name of the release
|
||||
Name string `json:"name,omitempty"`
|
||||
// Info provides information about a release
|
||||
// Info *Info `json:"info,omitempty"`
|
||||
// Chart is the chart that was released.
|
||||
Chart Chart `json:"chart,omitempty"`
|
||||
// Config is the set of extra Values added to the chart.
|
||||
// These values override the default values inside of the chart.
|
||||
Config map[string]interface{} `json:"config,omitempty"`
|
||||
// Manifest is the string representation of the rendered template.
|
||||
Manifest string `json:"manifest,omitempty"`
|
||||
// Hooks are all of the hooks declared for this release.
|
||||
Hooks []*Hook `json:"hooks,omitempty"`
|
||||
// Version is an int which represents the revision of the release.
|
||||
Version int `json:"version,omitempty"`
|
||||
// Namespace is the kubernetes namespace of the release.
|
||||
Namespace string `json:"namespace,omitempty"`
|
||||
// Labels of the release.
|
||||
// Disabled encoding into Json cause labels are stored in storage driver metadata field.
|
||||
Labels map[string]string `json:"-"`
|
||||
}
|
||||
|
||||
// Chart is a helm package that contains metadata, a default config, zero or more
|
||||
// optionally parameterizable templates, and zero or more charts (dependencies).
|
||||
type Chart struct {
|
||||
// Raw contains the raw contents of the files originally contained in the chart archive.
|
||||
//
|
||||
// This should not be used except in special cases like `helm show values`,
|
||||
// where we want to display the raw values, comments and all.
|
||||
Raw []*File `json:"-"`
|
||||
// Metadata is the contents of the Chartfile.
|
||||
Metadata *Metadata `json:"metadata"`
|
||||
// Lock is the contents of Chart.lock.
|
||||
Lock *Lock `json:"lock"`
|
||||
// Templates for this chart.
|
||||
Templates []*File `json:"templates"`
|
||||
// Values are default config for this chart.
|
||||
Values map[string]interface{} `json:"values"`
|
||||
// Schema is an optional JSON schema for imposing structure on Values
|
||||
Schema []byte `json:"schema"`
|
||||
// Files are miscellaneous files in a chart archive,
|
||||
// e.g. README, LICENSE, etc.
|
||||
Files []*File `json:"files"`
|
||||
|
||||
parent *Chart
|
||||
dependencies []*Chart
|
||||
}
|
||||
|
||||
// File represents a file as a name/value pair.
|
||||
//
|
||||
// By convention, name is a relative path within the scope of the chart's
|
||||
// base directory.
|
||||
type File struct {
|
||||
// Name is the path-like name of the template.
|
||||
Name string `json:"name"`
|
||||
// Data is the template as byte data.
|
||||
Data []byte `json:"data"`
|
||||
}
|
||||
|
||||
// Metadata for a Chart file. This models the structure of a Chart.yaml file.
|
||||
type Metadata struct {
|
||||
// The name of the chart. Required.
|
||||
Name string `json:"name,omitempty"`
|
||||
// The URL to a relevant project page, git repo, or contact person
|
||||
Home string `json:"home,omitempty"`
|
||||
// Source is the URL to the source code of this chart
|
||||
Sources []string `json:"sources,omitempty"`
|
||||
// A SemVer 2 conformant version string of the chart. Required.
|
||||
Version string `json:"version,omitempty"`
|
||||
// A one-sentence description of the chart
|
||||
Description string `json:"description,omitempty"`
|
||||
// A list of string keywords
|
||||
Keywords []string `json:"keywords,omitempty"`
|
||||
// A list of name and URL/email address combinations for the maintainer(s)
|
||||
Maintainers []*Maintainer `json:"maintainers,omitempty"`
|
||||
// The URL to an icon file.
|
||||
Icon string `json:"icon,omitempty"`
|
||||
// The API Version of this chart. Required.
|
||||
APIVersion string `json:"apiVersion,omitempty"`
|
||||
// The condition to check to enable chart
|
||||
Condition string `json:"condition,omitempty"`
|
||||
// The tags to check to enable chart
|
||||
Tags string `json:"tags,omitempty"`
|
||||
// The version of the application enclosed inside of this chart.
|
||||
AppVersion string `json:"appVersion,omitempty"`
|
||||
// Whether or not this chart is deprecated
|
||||
Deprecated bool `json:"deprecated,omitempty"`
|
||||
// Annotations are additional mappings uninterpreted by Helm,
|
||||
// made available for inspection by other applications.
|
||||
Annotations map[string]string `json:"annotations,omitempty"`
|
||||
// KubeVersion is a SemVer constraint specifying the version of Kubernetes required.
|
||||
KubeVersion string `json:"kubeVersion,omitempty"`
|
||||
// Dependencies are a list of dependencies for a chart.
|
||||
Dependencies []*Dependency `json:"dependencies,omitempty"`
|
||||
// Specifies the chart type: application or library
|
||||
Type string `json:"type,omitempty"`
|
||||
}
|
||||
|
||||
// Maintainer describes a Chart maintainer.
|
||||
type Maintainer struct {
|
||||
// Name is a user name or organization name
|
||||
Name string `json:"name,omitempty"`
|
||||
// Email is an optional email address to contact the named maintainer
|
||||
Email string `json:"email,omitempty"`
|
||||
// URL is an optional URL to an address for the named maintainer
|
||||
URL string `json:"url,omitempty"`
|
||||
}
|
||||
|
||||
// Dependency describes a chart upon which another chart depends.
|
||||
//
|
||||
// Dependencies can be used to express developer intent, or to capture the state
|
||||
// of a chart.
|
||||
type Dependency struct {
|
||||
// Name is the name of the dependency.
|
||||
//
|
||||
// This must mach the name in the dependency's Chart.yaml.
|
||||
Name string `json:"name"`
|
||||
// Version is the version (range) of this chart.
|
||||
//
|
||||
// A lock file will always produce a single version, while a dependency
|
||||
// may contain a semantic version range.
|
||||
Version string `json:"version,omitempty"`
|
||||
// The URL to the repository.
|
||||
//
|
||||
// Appending `index.yaml` to this string should result in a URL that can be
|
||||
// used to fetch the repository index.
|
||||
Repository string `json:"repository"`
|
||||
// A yaml path that resolves to a boolean, used for enabling/disabling charts (e.g. subchart1.enabled )
|
||||
Condition string `json:"condition,omitempty"`
|
||||
// Tags can be used to group charts for enabling/disabling together
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
// Enabled bool determines if chart should be loaded
|
||||
Enabled bool `json:"enabled,omitempty"`
|
||||
// ImportValues holds the mapping of source values to parent key to be imported. Each item can be a
|
||||
// string or pair of child/parent sublist items.
|
||||
ImportValues []interface{} `json:"import-values,omitempty"`
|
||||
// Alias usable alias to be used for the chart
|
||||
Alias string `json:"alias,omitempty"`
|
||||
}
|
||||
|
||||
// Lock is a lock file for dependencies.
|
||||
//
|
||||
// It represents the state that the dependencies should be in.
|
||||
type Lock struct {
|
||||
// Generated is the date the lock file was last generated.
|
||||
Generated time.Time `json:"generated"`
|
||||
// Digest is a hash of the dependencies in Chart.yaml.
|
||||
Digest string `json:"digest"`
|
||||
// Dependencies is the list of dependencies that this lock file has locked.
|
||||
Dependencies []*Dependency `json:"dependencies"`
|
||||
}
|
||||
|
||||
// Info describes release information.
|
||||
type Info struct {
|
||||
// FirstDeployed is when the release was first deployed.
|
||||
FirstDeployed time.Time `json:"first_deployed,omitempty"`
|
||||
// LastDeployed is when the release was last deployed.
|
||||
LastDeployed time.Time `json:"last_deployed,omitempty"`
|
||||
// Deleted tracks when this object was deleted.
|
||||
Deleted time.Time `json:"deleted"`
|
||||
// Description is human-friendly "log entry" about this release.
|
||||
Description string `json:"description,omitempty"`
|
||||
// Status is the current state of the release
|
||||
Status Status `json:"status,omitempty"`
|
||||
// Contains the rendered templates/NOTES.txt if available
|
||||
Notes string `json:"notes,omitempty"`
|
||||
}
|
||||
|
||||
// Status is the status of a release
|
||||
type Status string
|
||||
|
||||
// Hook defines a hook object.
|
||||
type Hook struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
// Kind is the Kubernetes kind.
|
||||
Kind string `json:"kind,omitempty"`
|
||||
// Path is the chart-relative path to the template.
|
||||
Path string `json:"path,omitempty"`
|
||||
// Manifest is the manifest contents.
|
||||
Manifest string `json:"manifest,omitempty"`
|
||||
// Events are the events that this hook fires on.
|
||||
Events []HookEvent `json:"events,omitempty"`
|
||||
// LastRun indicates the date/time this was last run.
|
||||
LastRun HookExecution `json:"last_run,omitempty"`
|
||||
// Weight indicates the sort order for execution among similar Hook type
|
||||
Weight int `json:"weight,omitempty"`
|
||||
// DeletePolicies are the policies that indicate when to delete the hook
|
||||
DeletePolicies []HookDeletePolicy `json:"delete_policies,omitempty"`
|
||||
}
|
||||
|
||||
// HookEvent specifies the hook event
|
||||
type HookEvent string
|
||||
|
||||
// A HookExecution records the result for the last execution of a hook for a given release.
|
||||
type HookExecution struct {
|
||||
// StartedAt indicates the date/time this hook was started
|
||||
StartedAt time.Time `json:"started_at,omitempty"`
|
||||
// CompletedAt indicates the date/time this hook was completed.
|
||||
CompletedAt time.Time `json:"completed_at,omitempty"`
|
||||
// Phase indicates whether the hook completed successfully
|
||||
Phase HookPhase `json:"phase"`
|
||||
}
|
||||
|
||||
// A HookPhase indicates the state of a hook execution
|
||||
type HookPhase string
|
||||
|
||||
// HookDeletePolicy specifies the hook delete policy
|
||||
type HookDeletePolicy string
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
Copyright The Helm Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Package time contains a wrapper for time.Time in the standard library and
|
||||
// associated methods. This package mainly exists to workaround an issue in Go
|
||||
// where the serializer doesn't omit an empty value for time:
|
||||
// https://github.com/golang/go/issues/11939. As such, this can be removed if a
|
||||
// proposal is ever accepted for Go
|
||||
package time
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"time"
|
||||
)
|
||||
|
||||
// emptyString contains an empty JSON string value to be used as output
|
||||
var emptyString = `""`
|
||||
|
||||
// Time is a convenience wrapper around stdlib time, but with different
|
||||
// marshalling and unmarshaling for zero values
|
||||
type Time struct {
|
||||
time.Time
|
||||
}
|
||||
|
||||
// Now returns the current time. It is a convenience wrapper around time.Now()
|
||||
func Now() Time {
|
||||
return Time{time.Now()}
|
||||
}
|
||||
|
||||
func (t Time) MarshalJSON() ([]byte, error) {
|
||||
if t.Time.IsZero() {
|
||||
return []byte(emptyString), nil
|
||||
}
|
||||
|
||||
return t.Time.MarshalJSON()
|
||||
}
|
||||
|
||||
func (t *Time) UnmarshalJSON(b []byte) error {
|
||||
if bytes.Equal(b, []byte("null")) {
|
||||
return nil
|
||||
}
|
||||
// If it is empty, we don't have to set anything since time.Time is not a
|
||||
// pointer and will be set to the zero value
|
||||
if bytes.Equal([]byte(emptyString), b) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return t.Time.UnmarshalJSON(b)
|
||||
}
|
||||
|
||||
func Parse(layout, value string) (Time, error) {
|
||||
t, err := time.Parse(layout, value)
|
||||
return Time{Time: t}, err
|
||||
}
|
||||
func ParseInLocation(layout, value string, loc *time.Location) (Time, error) {
|
||||
t, err := time.ParseInLocation(layout, value, loc)
|
||||
return Time{Time: t}, err
|
||||
}
|
||||
|
||||
func Date(year int, month time.Month, day, hour, min, sec, nsec int, loc *time.Location) Time {
|
||||
return Time{Time: time.Date(year, month, day, hour, min, sec, nsec, loc)}
|
||||
}
|
||||
|
||||
func Unix(sec int64, nsec int64) Time { return Time{Time: time.Unix(sec, nsec)} }
|
||||
|
||||
func (t Time) Add(d time.Duration) Time { return Time{Time: t.Time.Add(d)} }
|
||||
func (t Time) AddDate(years int, months int, days int) Time {
|
||||
return Time{Time: t.Time.AddDate(years, months, days)}
|
||||
}
|
||||
func (t Time) After(u Time) bool { return t.Time.After(u.Time) }
|
||||
func (t Time) Before(u Time) bool { return t.Time.Before(u.Time) }
|
||||
func (t Time) Equal(u Time) bool { return t.Time.Equal(u.Time) }
|
||||
func (t Time) In(loc *time.Location) Time { return Time{Time: t.Time.In(loc)} }
|
||||
func (t Time) Local() Time { return Time{Time: t.Time.Local()} }
|
||||
func (t Time) Round(d time.Duration) Time { return Time{Time: t.Time.Round(d)} }
|
||||
func (t Time) Sub(u Time) time.Duration { return t.Time.Sub(u.Time) }
|
||||
func (t Time) Truncate(d time.Duration) Time { return Time{Time: t.Time.Truncate(d)} }
|
||||
func (t Time) UTC() Time { return Time{Time: t.Time.UTC()} }
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
Copyright The Helm Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package time
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
testingTime, _ = Parse(time.RFC3339, "1977-09-02T22:04:05Z")
|
||||
testingTimeString = `"1977-09-02T22:04:05Z"`
|
||||
)
|
||||
|
||||
func TestNonZeroValueMarshal(t *testing.T) {
|
||||
res, err := json.Marshal(testingTime)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if testingTimeString != string(res) {
|
||||
t.Errorf("expected a marshaled value of %s, got %s", testingTimeString, res)
|
||||
}
|
||||
}
|
||||
|
||||
func TestZeroValueMarshal(t *testing.T) {
|
||||
res, err := json.Marshal(Time{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(res) != emptyString {
|
||||
t.Errorf("expected zero value to marshal to empty string, got %s", res)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNonZeroValueUnmarshal(t *testing.T) {
|
||||
var myTime Time
|
||||
err := json.Unmarshal([]byte(testingTimeString), &myTime)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !myTime.Equal(testingTime) {
|
||||
t.Errorf("expected time to be equal to %v, got %v", testingTime, myTime)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmptyStringUnmarshal(t *testing.T) {
|
||||
var myTime Time
|
||||
err := json.Unmarshal([]byte(emptyString), &myTime)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !myTime.IsZero() {
|
||||
t.Errorf("expected time to be equal to zero value, got %v", myTime)
|
||||
}
|
||||
}
|
||||
|
||||
func TestZeroValueUnmarshal(t *testing.T) {
|
||||
// This test ensures that we can unmarshal any time value that was output
|
||||
// with the current go default value of "0001-01-01T00:00:00Z"
|
||||
var myTime Time
|
||||
err := json.Unmarshal([]byte(`"0001-01-01T00:00:00Z"`), &myTime)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !myTime.IsZero() {
|
||||
t.Errorf("expected time to be equal to zero value, got %v", myTime)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
package libhelm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const invalidChartRepo = "%q is not a valid chart repository or cannot be reached"
|
||||
|
||||
func ValidateHelmRepositoryURL(repoUrl string) error {
|
||||
if repoUrl == "" {
|
||||
return errors.New("URL is required")
|
||||
}
|
||||
|
||||
url, err := url.ParseRequestURI(repoUrl)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("invalid helm chart URL: %s", repoUrl))
|
||||
}
|
||||
|
||||
if !strings.EqualFold(url.Scheme, "http") && !strings.EqualFold(url.Scheme, "https") {
|
||||
return errors.New(fmt.Sprintf("invalid helm chart URL: %s", repoUrl))
|
||||
}
|
||||
|
||||
url.Path = path.Join(url.Path, "index.yaml")
|
||||
|
||||
var client = &http.Client{
|
||||
Timeout: time.Second * 10,
|
||||
}
|
||||
response, err := client.Head(url.String())
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, invalidChartRepo, repoUrl)
|
||||
}
|
||||
|
||||
// Success is indicated with 2xx status codes
|
||||
statusOK := response.StatusCode >= 200 && response.StatusCode < 300
|
||||
if !statusOK {
|
||||
return errors.Errorf(invalidChartRepo, repoUrl)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
package libhelm
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/portainer/libhelm/libhelmtest"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_ValidateHelmRepositoryURL(t *testing.T) {
|
||||
libhelmtest.EnsureIntegrationTest(t)
|
||||
is := assert.New(t)
|
||||
|
||||
type testCase struct {
|
||||
name string
|
||||
url string
|
||||
invalid bool
|
||||
}
|
||||
|
||||
tests := []testCase{
|
||||
{"blank", "", true},
|
||||
{"slashes", "//", true},
|
||||
{"slash", "/", true},
|
||||
{"invalid scheme", "garbage://a.b.c", true},
|
||||
{"invalid domain", "https://invaliddomain/", true},
|
||||
{"not helm repo", "http://google.com", true},
|
||||
{"not valid repo with trailing slash", "http://google.com/", true},
|
||||
{"not valid repo with trailing slashes", "http://google.com////", true},
|
||||
{"bitnami helm repo", "https://charts.bitnami.com/bitnami/", false},
|
||||
{"gitlap helm repo", "https://charts.gitlab.io/", false},
|
||||
{"portainer helm repo", "https://portainer.github.io/k8s/", false},
|
||||
{"elastic helm repo", "https://helm.elastic.co/", false},
|
||||
{"redirect", "https://charts.jetstack.io/", false},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
func(tc testCase) {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
err := ValidateHelmRepositoryURL(tc.url)
|
||||
if tc.invalid {
|
||||
is.Errorf(err, "error expected: %s", tc.url)
|
||||
} else {
|
||||
is.NoError(err, "no error expected: %s", tc.url)
|
||||
}
|
||||
})
|
||||
}(test)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue