Skip to content

Commit e11c0d9

Browse files
committed
Add experimental support for release testing
1 parent 34acc1f commit e11c0d9

22 files changed

+843
-8
lines changed

cli/cmd/app_create.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,8 @@ func (r *runners) createApp(_ *cobra.Command, args []string) error {
3030
kotsRestClient := kotsclient.VendorV3Client{HTTPClient: *r.platformAPI}
3131

3232
app, err := kotsRestClient.CreateKOTSApp(appName)
33-
3433
if err != nil {
35-
return errors.Wrap(err, "list apps")
34+
return errors.Wrap(err, "create app")
3635
}
3736

3837
apps := []types.AppAndChannels{

cli/cmd/cluster.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package cmd
2+
3+
import (
4+
"github.com/spf13/cobra"
5+
)
6+
7+
func (r *runners) InitClusterCommand(parent *cobra.Command) *cobra.Command {
8+
cmd := &cobra.Command{
9+
Use: "cluster",
10+
Short: "Manage test clusters",
11+
Long: ``,
12+
Hidden: true,
13+
}
14+
parent.AddCommand(cmd)
15+
16+
return cmd
17+
}

cli/cmd/cluster_create.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package cmd
2+
3+
import (
4+
"github.com/pkg/errors"
5+
"github.com/replicatedhq/replicated/pkg/kotsclient"
6+
"github.com/replicatedhq/replicated/pkg/platformclient"
7+
"github.com/spf13/cobra"
8+
)
9+
10+
func (r *runners) InitClusterCreate(parent *cobra.Command) *cobra.Command {
11+
cmd := &cobra.Command{
12+
Use: "create",
13+
Short: "create test clusters",
14+
Long: `create test clusters`,
15+
RunE: r.createCluster,
16+
SilenceUsage: true,
17+
}
18+
parent.AddCommand(cmd)
19+
20+
cmd.Flags().StringVar(&r.args.createClusterName, "name", "", "cluster name")
21+
cmd.MarkFlagRequired("name")
22+
23+
cmd.Flags().StringVar(&r.args.createClusterKubernetesDistribution, "kubernetes-distribution", "kind", "Kubernetes distribution of the cluster to provision")
24+
cmd.Flags().StringVar(&r.args.createClusterKubernetesVersion, "kubernetes-version", "1.25.3", "Kubernetes version to provision (format is distribution dependent)")
25+
cmd.Flags().IntVar(&r.args.createClusterNodeCount, "node-count", int(1), "Node count")
26+
cmd.Flags().Int64Var(&r.args.createClusterVCpus, "vcpus", int64(4), "vCPUs to request per node")
27+
cmd.Flags().Int64Var(&r.args.createClusterMemoryMiB, "memory-mib", int64(4096), "Memory (MiB) to request per node")
28+
cmd.Flags().StringVar(&r.args.createClusterTTL, "ttl", "1h", "Cluster TTL (duration)")
29+
30+
return cmd
31+
}
32+
33+
func (r *runners) createCluster(_ *cobra.Command, args []string) error {
34+
kotsRestClient := kotsclient.VendorV3Client{HTTPClient: *r.platformAPI}
35+
36+
opts := kotsclient.CreateClusterOpts{
37+
Name: r.args.createClusterName,
38+
KubernetesDistribution: r.args.createClusterKubernetesDistribution,
39+
KubernetesVersion: r.args.createClusterKubernetesVersion,
40+
NodeCount: r.args.createClusterNodeCount,
41+
VCpus: r.args.createClusterVCpus,
42+
MemoryMiB: r.args.createClusterMemoryMiB,
43+
TTL: r.args.createClusterTTL,
44+
}
45+
_, err := kotsRestClient.CreateCluster(opts)
46+
if errors.Cause(err) == platformclient.ErrForbidden {
47+
return errors.New("This command is not available for your account or team. Please contact your customer success representative for more information.")
48+
}
49+
if err != nil {
50+
return errors.Wrap(err, "create cluster")
51+
}
52+
53+
return nil
54+
}

cli/cmd/cluster_ls.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package cmd
2+
3+
import (
4+
"github.com/pkg/errors"
5+
"github.com/replicatedhq/replicated/cli/print"
6+
"github.com/replicatedhq/replicated/pkg/kotsclient"
7+
"github.com/replicatedhq/replicated/pkg/platformclient"
8+
"github.com/spf13/cobra"
9+
)
10+
11+
func (r *runners) InitClusterList(parent *cobra.Command) *cobra.Command {
12+
cmd := &cobra.Command{
13+
Use: "ls",
14+
Short: "list test clusters",
15+
Long: `list test clusters`,
16+
RunE: r.listClusters,
17+
SilenceUsage: true,
18+
}
19+
parent.AddCommand(cmd)
20+
21+
return cmd
22+
}
23+
24+
func (r *runners) listClusters(_ *cobra.Command, args []string) error {
25+
kotsRestClient := kotsclient.VendorV3Client{HTTPClient: *r.platformAPI}
26+
27+
clusters, err := kotsRestClient.ListClusters()
28+
if err == platformclient.ErrForbidden {
29+
return errors.New("This command is not available for your account or team. Please contact your customer success representative for more information.")
30+
}
31+
if err != nil {
32+
return errors.Wrap(err, "list clusters")
33+
}
34+
35+
if len(clusters) == 0 {
36+
return print.NoClusters(r.w)
37+
}
38+
39+
return print.Clusters(r.w, clusters)
40+
}

cli/cmd/cluster_rm.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package cmd
2+
3+
import (
4+
"github.com/pkg/errors"
5+
"github.com/replicatedhq/replicated/pkg/kotsclient"
6+
"github.com/spf13/cobra"
7+
)
8+
9+
func (r *runners) InitClusterRemove(parent *cobra.Command) *cobra.Command {
10+
cmd := &cobra.Command{
11+
Use: "rm",
12+
Short: "remove test clusters",
13+
Long: `remove test clusters`,
14+
RunE: r.removeCluster,
15+
SilenceUsage: true,
16+
}
17+
parent.AddCommand(cmd)
18+
19+
cmd.Flags().StringVar(&r.args.removeClusterID, "id", "", "cluster id")
20+
cmd.MarkFlagRequired("id")
21+
22+
cmd.Flags().BoolVar(&r.args.removeClusterForce, "force", false, "force remove cluster")
23+
24+
return cmd
25+
}
26+
27+
func (r *runners) removeCluster(_ *cobra.Command, args []string) error {
28+
kotsRestClient := kotsclient.VendorV3Client{HTTPClient: *r.platformAPI}
29+
30+
err := kotsRestClient.RemoveCluster(r.args.removeClusterID, r.args.removeClusterForce)
31+
if err != nil {
32+
return errors.Wrap(err, "remove cluster")
33+
}
34+
35+
return nil
36+
}

cli/cmd/cluster_update_config.go

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"io/fs"
6+
"io/ioutil"
7+
"os"
8+
"path/filepath"
9+
"runtime"
10+
"strings"
11+
12+
"github.com/pkg/errors"
13+
"github.com/replicatedhq/replicated/pkg/kotsclient"
14+
"github.com/spf13/cobra"
15+
"k8s.io/client-go/tools/clientcmd"
16+
)
17+
18+
const (
19+
homeEnvVar = "HOME"
20+
KubeConfigEnvVar = "KUBECONFIG"
21+
)
22+
23+
func (r *runners) InitClusterKubeconfig(parent *cobra.Command) *cobra.Command {
24+
cmd := &cobra.Command{
25+
Use: "kubeconfig",
26+
Short: "Download credentials for a test cluster",
27+
Long: `Download credentials for a test cluster`,
28+
RunE: r.kubeconfigCluster,
29+
SilenceUsage: true,
30+
}
31+
parent.AddCommand(cmd)
32+
33+
cmd.Flags().StringVar(&r.args.kubeconfigClusterID, "id", "", "cluster id")
34+
cmd.MarkFlagRequired("id")
35+
36+
return cmd
37+
}
38+
39+
func (r *runners) kubeconfigCluster(_ *cobra.Command, args []string) error {
40+
kotsRestClient := kotsclient.VendorV3Client{HTTPClient: *r.platformAPI}
41+
42+
kubeconfig, err := kotsRestClient.GetClusterKubeconfig(r.args.kubeconfigClusterID)
43+
if err != nil {
44+
return errors.Wrap(err, "get cluster kubeconfig")
45+
}
46+
tmpFile, err := ioutil.TempFile("", "replicated-kubeconfig")
47+
if err != nil {
48+
return errors.Wrap(err, "create temp file")
49+
}
50+
defer os.Remove(tmpFile.Name())
51+
if err := ioutil.WriteFile(tmpFile.Name(), kubeconfig, 0644); err != nil {
52+
return errors.Wrap(err, "write temp file")
53+
}
54+
55+
replicatedLoadingRules := clientcmd.ClientConfigLoadingRules{
56+
ExplicitPath: tmpFile.Name(),
57+
}
58+
replicatedConfig, err := replicatedLoadingRules.Load()
59+
if err != nil {
60+
return errors.Wrap(err, "load kubeconfig")
61+
}
62+
63+
kubeconfigPaths := getKubeconfigPaths()
64+
backupPaths := []string{}
65+
66+
// back up the curent kubeconfigs
67+
for _, kubeconfigPath := range kubeconfigPaths {
68+
backupPath := kubeconfigPath + ".replicated_backup"
69+
70+
fi, err := os.Stat(kubeconfigPath)
71+
var pathError *fs.PathError
72+
if errors.As(err, &pathError) {
73+
// file doesn't exist, nothing to backup
74+
continue
75+
} else if err != nil {
76+
return errors.Wrap(err, "stat kubeconfig")
77+
}
78+
data, err := ioutil.ReadFile(kubeconfigPath)
79+
if err != nil {
80+
return errors.Wrap(err, "read kubeconfig")
81+
}
82+
83+
if err := ioutil.WriteFile(backupPath, data, fi.Mode()); err != nil {
84+
return errors.Wrap(err, "write backup kubeconfig")
85+
}
86+
87+
backupPaths = append(backupPaths, backupPath)
88+
}
89+
defer func() {
90+
for _, backupPath := range backupPaths {
91+
err := os.Remove(backupPath)
92+
if err != nil {
93+
fmt.Printf("failed to remove backup kubeconfig: %s\n", err.Error())
94+
}
95+
}
96+
}()
97+
98+
// parse the current kubeconfig
99+
loadingRules := clientcmd.ClientConfigLoadingRules{
100+
Precedence: kubeconfigPaths,
101+
}
102+
mergedConfig, err := loadingRules.Load()
103+
if err != nil {
104+
return errors.Wrap(err, "load kubeconfig")
105+
}
106+
107+
// add the replicated context
108+
for contextName, context := range replicatedConfig.Contexts {
109+
mergedConfig.Contexts[contextName] = context
110+
}
111+
// add the replicated credentials
112+
for credentialName, credential := range replicatedConfig.AuthInfos {
113+
mergedConfig.AuthInfos[credentialName] = credential
114+
}
115+
// add the replicated cluster
116+
for clusterName, cluster := range replicatedConfig.Clusters {
117+
mergedConfig.Clusters[clusterName] = cluster
118+
}
119+
120+
mergedConfig.CurrentContext = replicatedConfig.CurrentContext
121+
122+
// write the merged kubeconfig
123+
err = clientcmd.WriteToFile(*mergedConfig, kubeconfigPaths[0])
124+
if err != nil {
125+
return errors.Wrap(err, "write kubeconfig")
126+
}
127+
128+
fmt.Printf(" ✓ Updated kubernetes context '%s' in '%s'\n", mergedConfig.CurrentContext, kubeconfigPaths[0])
129+
130+
return nil
131+
}
132+
133+
func getKubeconfigPaths() []string {
134+
home := getHomeDir()
135+
kubeconfig := []string{filepath.Join(home, ".kube", "config")}
136+
kubeconfigEnv := os.Getenv(KubeConfigEnvVar)
137+
if len(kubeconfigEnv) > 0 {
138+
kubeconfig = splitKubeConfigEnv(kubeconfigEnv)
139+
}
140+
141+
return kubeconfig
142+
}
143+
144+
func getHomeDir() string {
145+
if h := os.Getenv("HOME"); h != "" {
146+
return h
147+
}
148+
return os.Getenv("USERPROFILE")
149+
}
150+
151+
func splitKubeConfigEnv(value string) []string {
152+
if runtime.GOOS == "windows" {
153+
return strings.Split(value, ";")
154+
}
155+
return strings.Split(value, ":")
156+
}

cli/cmd/release_validate.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"strconv"
6+
7+
"github.com/pkg/errors"
8+
"github.com/spf13/cobra"
9+
)
10+
11+
func (r *runners) InitReleaseTest(parent *cobra.Command) {
12+
cmd := &cobra.Command{
13+
Use: "test SEQUENCE",
14+
Short: "Test the application release",
15+
Long: "Test the application release",
16+
}
17+
parent.AddCommand(cmd)
18+
cmd.RunE = r.releaseTest
19+
}
20+
21+
func (r *runners) releaseTest(command *cobra.Command, args []string) error {
22+
if len(args) != 1 {
23+
return errors.New("release sequence is required")
24+
}
25+
seq, err := strconv.ParseInt(args[0], 10, 64)
26+
if err != nil {
27+
return fmt.Errorf("Failed to parse sequence argument %q", args[0])
28+
}
29+
30+
result, err := r.api.TestRelease(r.appID, r.appType, seq)
31+
if err != nil {
32+
return errors.Wrap(err, "test release")
33+
}
34+
35+
fmt.Printf("Test results for release %#v", result)
36+
37+
return nil
38+
39+
}

cli/cmd/root.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ func Execute(rootCmd *cobra.Command, stdin io.Reader, stdout io.Writer, stderr i
149149
runCmds.InitReleaseUpdate(releaseCmd)
150150
runCmds.InitReleasePromote(releaseCmd)
151151
runCmds.InitReleaseLint(releaseCmd)
152+
runCmds.InitReleaseTest(releaseCmd)
152153

153154
collectorsCmd := runCmds.InitCollectorsCommand(runCmds.rootCmd)
154155
runCmds.InitCollectorList(collectorsCmd)
@@ -213,6 +214,12 @@ func Execute(rootCmd *cobra.Command, stdin io.Reader, stdout io.Writer, stderr i
213214
runCmds.InitRegistryAddQuay(registryAddCmd)
214215
runCmds.InitRegistryAddOther(registryAddCmd)
215216

217+
clusterCmd := runCmds.InitClusterCommand(runCmds.rootCmd)
218+
runCmds.InitClusterCreate(clusterCmd)
219+
runCmds.InitClusterList(clusterCmd)
220+
runCmds.InitClusterKubeconfig(clusterCmd)
221+
runCmds.InitClusterRemove(clusterCmd)
222+
216223
runCmds.rootCmd.SetUsageTemplate(rootCmdUsageTmpl)
217224

218225
preRunSetupAPIs := func(_ *cobra.Command, _ []string) error {
@@ -276,6 +283,7 @@ func Execute(rootCmd *cobra.Command, stdin io.Reader, stdout io.Writer, stderr i
276283
installerCmd.PersistentPreRunE = prerunCommand
277284
appCmd.PersistentPreRunE = preRunSetupAPIs
278285
registryCmd.PersistentPreRunE = preRunSetupAPIs
286+
clusterCmd.PersistentPreRunE = preRunSetupAPIs
279287

280288
runCmds.rootCmd.AddCommand(Version())
281289

0 commit comments

Comments
 (0)