diff --git a/cmd/clusterctl/clusterdeployer/clusterdeployer.go b/cmd/clusterctl/clusterdeployer/clusterdeployer.go index fc9367b75e0d..66509685f872 100644 --- a/cmd/clusterctl/clusterdeployer/clusterdeployer.go +++ b/cmd/clusterctl/clusterdeployer/clusterdeployer.go @@ -92,25 +92,24 @@ func (d *ClusterDeployer) Create(cluster *clusterv1.Cluster, machines []*cluster } defer closeClient(bootstrapClient, "bootstrap") - if cluster.Namespace == "" { - cluster.Namespace = bootstrapClient.GetContextNamespace() - } - - err = bootstrapClient.EnsureNamespace(cluster.Namespace) - if err != nil { - return fmt.Errorf("unable to ensure namespace %q in bootstrap cluster: %v", cluster.Namespace, err) - } - glog.Info("Applying Cluster API stack to bootstrap cluster") if err := phases.ApplyClusterAPIComponents(bootstrapClient, d.providerComponents); err != nil { return fmt.Errorf("unable to apply cluster api stack to bootstrap cluster: %v", err) } glog.Info("Provisioning target cluster via bootstrap cluster") + if err := phases.ApplyCluster(bootstrapClient, cluster); err != nil { + return fmt.Errorf("unable to create cluster %q in bootstrap cluster: %v", cluster.Name, err) + } + + // Create initial controlplane instance + if cluster.Namespace == "" { + cluster.Namespace = bootstrapClient.GetContextNamespace() + } - glog.Infof("Creating cluster object %v on bootstrap cluster in namespace %q", cluster.Name, cluster.Namespace) - if err := bootstrapClient.CreateClusterObject(cluster); err != nil { - return fmt.Errorf("unable to create cluster object: %v", err) + err = bootstrapClient.EnsureNamespace(cluster.Namespace) + if err != nil { + return fmt.Errorf("unable to ensure namespace %q in bootstrap cluster: %v", cluster.Namespace, err) } glog.Infof("Creating master %v in namespace %q", master.Name, cluster.Namespace) diff --git a/cmd/clusterctl/cmd/alpha_phase_apply_cluster.go b/cmd/clusterctl/cmd/alpha_phase_apply_cluster.go new file mode 100644 index 000000000000..4df1bfd622ec --- /dev/null +++ b/cmd/clusterctl/cmd/alpha_phase_apply_cluster.go @@ -0,0 +1,85 @@ +/* +Copyright 2018 The Kubernetes 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 cmd + +import ( + "fmt" + "io/ioutil" + + "github.com/golang/glog" + "github.com/spf13/cobra" + "sigs.k8s.io/cluster-api/cmd/clusterctl/clusterdeployer/clusterclient" + "sigs.k8s.io/cluster-api/cmd/clusterctl/phases" + "sigs.k8s.io/cluster-api/pkg/util" +) + +type AlphaPhaseApplyClusterOptions struct { + Kubeconfig string + Cluster string +} + +var paco = &AlphaPhaseApplyClusterOptions{} + +var alphaPhaseApplyClusterCmd = &cobra.Command{ + Use: "apply-cluster", + Short: "Apply Cluster", + Long: `Apply Cluster`, + Run: func(cmd *cobra.Command, args []string) { + if paco.Cluster == "" { + exitWithHelp(cmd, "Please provide yaml file for cluster definition.") + } + + if paco.Kubeconfig == "" { + exitWithHelp(cmd, "Please provide a kubeconfig file.") + } + + if err := RunAlphaPhaseApplyCluster(paco); err != nil { + glog.Exit(err) + } + }, +} + +func RunAlphaPhaseApplyCluster(paco *AlphaPhaseApplyClusterOptions) error { + kubeconfig, err := ioutil.ReadFile(paco.Kubeconfig) + if err != nil { + return err + } + + cluster, err := util.ParseClusterYaml(paco.Cluster) + if err != nil { + return err + } + + clientFactory := clusterclient.NewFactory() + client, err := clientFactory.NewClientFromKubeconfig(string(kubeconfig)) + if err != nil { + return fmt.Errorf("unable to create cluster client: %v", err) + } + + if err := phases.ApplyCluster(client, cluster); err != nil { + return fmt.Errorf("unable to apply cluster: %v", err) + } + + return nil +} + +func init() { + // Required flags + alphaPhaseApplyClusterCmd.Flags().StringVarP(&paco.Kubeconfig, "kubeconfig", "", "", "Path for the kubeconfig file to use") + alphaPhaseApplyClusterCmd.Flags().StringVarP(&paco.Cluster, "cluster", "c", "", "A yaml file containing cluster object definition") + alphaPhasesCmd.AddCommand(alphaPhaseApplyClusterCmd) +} diff --git a/cmd/clusterctl/cmd/create_cluster.go b/cmd/clusterctl/cmd/create_cluster.go index 873b3799ad52..eab65a788766 100644 --- a/cmd/clusterctl/cmd/create_cluster.go +++ b/cmd/clusterctl/cmd/create_cluster.go @@ -28,7 +28,6 @@ import ( "sigs.k8s.io/cluster-api/cmd/clusterctl/clusterdeployer/bootstrap/minikube" "sigs.k8s.io/cluster-api/cmd/clusterctl/clusterdeployer/clusterclient" clustercommon "sigs.k8s.io/cluster-api/pkg/apis/cluster/common" - clusterv1 "sigs.k8s.io/cluster-api/pkg/apis/cluster/v1alpha1" "sigs.k8s.io/cluster-api/pkg/util" "sigs.k8s.io/yaml" ) @@ -69,11 +68,11 @@ var createClusterCmd = &cobra.Command{ } func RunCreate(co *CreateOptions) error { - c, err := parseClusterYaml(co.Cluster) + c, err := util.ParseClusterYaml(co.Cluster) if err != nil { return err } - m, err := parseMachinesYaml(co.Machine) + m, err := util.ParseMachinesYaml(co.Machine) if err != nil { return err } @@ -137,40 +136,6 @@ func init() { createCmd.AddCommand(createClusterCmd) } -func parseClusterYaml(file string) (*clusterv1.Cluster, error) { - bytes, err := ioutil.ReadFile(file) - if err != nil { - return nil, err - } - - cluster := &clusterv1.Cluster{} - err = yaml.Unmarshal(bytes, cluster) - if err != nil { - return nil, err - } - - return cluster, nil -} - -func parseMachinesYaml(file string) ([]*clusterv1.Machine, error) { - bytes, err := ioutil.ReadFile(file) - if err != nil { - return nil, err - } - - list := &clusterv1.MachineList{} - err = yaml.Unmarshal(bytes, &list) - if err != nil { - return nil, err - } - - if list == nil { - return []*clusterv1.Machine{}, nil - } - - return util.MachineP(list.Items), nil -} - func getProvider(name string) (clusterdeployer.ProviderDeployer, error) { provisioner, err := clustercommon.ClusterProvisioner(name) if err != nil { diff --git a/cmd/clusterctl/cmd/create_cluster_test.go b/cmd/clusterctl/cmd/create_cluster_test.go index 1dede9b0362b..dd2142cba629 100644 --- a/cmd/clusterctl/cmd/create_cluster_test.go +++ b/cmd/clusterctl/cmd/create_cluster_test.go @@ -17,124 +17,9 @@ limitations under the License. package cmd import ( - "io/ioutil" - "os" "testing" ) -const validCluster = ` -apiVersion: "cluster.k8s.io/v1alpha1" -kind: Cluster -metadata: - name: cluster1 -spec:` - -const validMachines = ` -items: -- apiVersion: "cluster.k8s.io/v1alpha1" - kind: Machine - metadata: - name: machine1 - spec:` - -func TestParseClusterYaml(t *testing.T) { - t.Run("File does not exist", func(t *testing.T) { - _, err := parseClusterYaml("fileDoesNotExist") - if err == nil { - t.Fatal("Was able to parse a file that does not exist") - } - }) - var testcases = []struct { - name string - contents string - expectedName string - expectErr bool - }{ - { - name: "valid file", - contents: validCluster, - expectedName: "cluster1", - }, - { - name: "gibberish in file", - contents: `blah ` + validCluster + ` blah`, - expectErr: true, - }, - } - for _, testcase := range testcases { - t.Run(testcase.name, func(t *testing.T) { - file, err := createTempFile(testcase.contents) - if err != nil { - t.Fatal(err) - } - defer os.Remove(file) - - c, err := parseClusterYaml(file) - if (testcase.expectErr && err == nil) || (!testcase.expectErr && err != nil) { - t.Fatalf("Unexpected returned error. Got: %v, Want Err: %v", err, testcase.expectErr) - } - if err != nil { - return - } - if c == nil { - t.Fatalf("No cluster returned in success case.") - } - if c.Name != testcase.expectedName { - t.Fatalf("Unexpected name. Got: %v, Want:%v", c.Name, testcase.expectedName) - } - }) - } -} - -func TestParseMachineYaml(t *testing.T) { - t.Run("File does not exist", func(t *testing.T) { - _, err := parseMachinesYaml("fileDoesNotExist") - if err == nil { - t.Fatal("Was able to parse a file that does not exist") - } - }) - var testcases = []struct { - name string - contents string - expectErr bool - expectedMachineCount int - }{ - { - name: "valid file", - contents: validMachines, - expectedMachineCount: 1, - }, - { - name: "gibberish in file", - contents: `blah ` + validMachines + ` blah`, - expectErr: true, - }, - } - for _, testcase := range testcases { - t.Run(testcase.name, func(t *testing.T) { - file, err := createTempFile(testcase.contents) - if err != nil { - t.Fatal(err) - } - defer os.Remove(file) - - m, err := parseMachinesYaml(file) - if (testcase.expectErr && err == nil) || (!testcase.expectErr && err != nil) { - t.Fatalf("Unexpected returned error. Got: %v, Want Err: %v", err, testcase.expectErr) - } - if err != nil { - return - } - if m == nil { - t.Fatalf("No machines returned in success case.") - } - if len(m) != testcase.expectedMachineCount { - t.Fatalf("Unexpected machine count. Got: %v, Want: %v", len(m), testcase.expectedMachineCount) - } - }) - } -} - func TestGetProvider(t *testing.T) { var testcases = []struct { provider string @@ -154,13 +39,3 @@ func TestGetProvider(t *testing.T) { }) } } - -func createTempFile(contents string) (string, error) { - f, err := ioutil.TempFile("", "") - if err != nil { - return "", err - } - defer f.Close() - f.WriteString(contents) - return f.Name(), nil -} diff --git a/cmd/clusterctl/phases/applycluster.go b/cmd/clusterctl/phases/applycluster.go new file mode 100644 index 000000000000..567ea7120fc2 --- /dev/null +++ b/cmd/clusterctl/phases/applycluster.go @@ -0,0 +1,43 @@ +/* +Copyright 2018 The Kubernetes 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 phases + +import ( + "fmt" + + "github.com/golang/glog" + "sigs.k8s.io/cluster-api/cmd/clusterctl/clusterdeployer/clusterclient" + clusterv1 "sigs.k8s.io/cluster-api/pkg/apis/cluster/v1alpha1" +) + +func ApplyCluster(client clusterclient.Client, cluster *clusterv1.Cluster) error { + if cluster.Namespace == "" { + cluster.Namespace = client.GetContextNamespace() + } + + err := client.EnsureNamespace(cluster.Namespace) + if err != nil { + return fmt.Errorf("unable to ensure namespace %q: %v", cluster.Namespace, err) + } + + glog.Infof("Creating cluster object %v in namespace %q", cluster.Name, cluster.Namespace) + if err := client.CreateClusterObject(cluster); err != nil { + return err + } + + return nil +} diff --git a/pkg/util/util.go b/pkg/util/util.go index 03763663a001..c57fc587baed 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -19,6 +19,7 @@ package util import ( "context" "fmt" + "io/ioutil" "math/rand" "os" "os/exec" @@ -26,12 +27,12 @@ import ( "strings" "time" + "github.com/ghodss/yaml" "github.com/golang/glog" "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" - "sigs.k8s.io/controller-runtime/pkg/client" - clusterv1 "sigs.k8s.io/cluster-api/pkg/apis/cluster/v1alpha1" + "sigs.k8s.io/controller-runtime/pkg/client" ) const ( @@ -171,3 +172,35 @@ func GetNamespaceOrDefault(namespace string) string { } return namespace } + +func ParseClusterYaml(file string) (*clusterv1.Cluster, error) { + bytes, err := ioutil.ReadFile(file) + if err != nil { + return nil, err + } + + cluster := &clusterv1.Cluster{} + if err := yaml.Unmarshal(bytes, cluster); err != nil { + return nil, err + } + + return cluster, nil +} + +func ParseMachinesYaml(file string) ([]*clusterv1.Machine, error) { + bytes, err := ioutil.ReadFile(file) + if err != nil { + return nil, err + } + + list := &clusterv1.MachineList{} + if err := yaml.Unmarshal(bytes, &list); err != nil { + return nil, err + } + + if list == nil { + return []*clusterv1.Machine{}, nil + } + + return MachineP(list.Items), nil +} diff --git a/pkg/util/util_test.go b/pkg/util/util_test.go new file mode 100644 index 000000000000..abadc91f9c04 --- /dev/null +++ b/pkg/util/util_test.go @@ -0,0 +1,146 @@ +/* +Copyright 2018 The Kubernetes 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 util + +import ( + "io/ioutil" + "os" + "testing" +) + +const validCluster = ` +apiVersion: "cluster.k8s.io/v1alpha1" +kind: Cluster +metadata: + name: cluster1 +spec:` + +const validMachines = ` +items: +- apiVersion: "cluster.k8s.io/v1alpha1" + kind: Machine + metadata: + name: machine1 + spec:` + +func TestParseClusterYaml(t *testing.T) { + t.Run("File does not exist", func(t *testing.T) { + _, err := ParseClusterYaml("fileDoesNotExist") + if err == nil { + t.Fatal("Was able to parse a file that does not exist") + } + }) + var testcases = []struct { + name string + contents string + expectedName string + expectErr bool + }{ + { + name: "valid file", + contents: validCluster, + expectedName: "cluster1", + }, + { + name: "gibberish in file", + contents: `blah ` + validCluster + ` blah`, + expectErr: true, + }, + } + for _, testcase := range testcases { + t.Run(testcase.name, func(t *testing.T) { + file, err := createTempFile(testcase.contents) + if err != nil { + t.Fatal(err) + } + defer os.Remove(file) + + c, err := ParseClusterYaml(file) + if (testcase.expectErr && err == nil) || (!testcase.expectErr && err != nil) { + t.Fatalf("Unexpected returned error. Got: %v, Want Err: %v", err, testcase.expectErr) + } + if err != nil { + return + } + if c == nil { + t.Fatalf("No cluster returned in success case.") + } + if c.Name != testcase.expectedName { + t.Fatalf("Unexpected name. Got: %v, Want:%v", c.Name, testcase.expectedName) + } + }) + } +} + +func TestParseMachineYaml(t *testing.T) { + t.Run("File does not exist", func(t *testing.T) { + _, err := ParseMachinesYaml("fileDoesNotExist") + if err == nil { + t.Fatal("Was able to parse a file that does not exist") + } + }) + var testcases = []struct { + name string + contents string + expectErr bool + expectedMachineCount int + }{ + { + name: "valid file", + contents: validMachines, + expectedMachineCount: 1, + }, + { + name: "gibberish in file", + contents: `blah ` + validMachines + ` blah`, + expectErr: true, + }, + } + for _, testcase := range testcases { + t.Run(testcase.name, func(t *testing.T) { + file, err := createTempFile(testcase.contents) + if err != nil { + t.Fatal(err) + } + defer os.Remove(file) + + m, err := ParseMachinesYaml(file) + if (testcase.expectErr && err == nil) || (!testcase.expectErr && err != nil) { + t.Fatalf("Unexpected returned error. Got: %v, Want Err: %v", err, testcase.expectErr) + } + if err != nil { + return + } + if m == nil { + t.Fatalf("No machines returned in success case.") + } + if len(m) != testcase.expectedMachineCount { + t.Fatalf("Unexpected machine count. Got: %v, Want: %v", len(m), testcase.expectedMachineCount) + } + }) + } +} + +func createTempFile(contents string) (string, error) { + f, err := ioutil.TempFile("", "") + if err != nil { + return "", err + } + defer f.Close() + f.WriteString(contents) + return f.Name(), nil +}