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

Create YAML or JSON Clusterspec without creating the cluster #2954

Merged
Merged
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
1 change: 1 addition & 0 deletions cmd/kops/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ func RunCreate(f *util.Factory, out io.Writer, c *CreateOptions) error {
return fmt.Errorf("error reading file %q: %v", f, err)
}

// TODO: this does not support a JSON array
sections := bytes.Split(contents, []byte("\n---\n"))
for _, section := range sections {
defaults := &schema.GroupVersionKind{
Expand Down
49 changes: 48 additions & 1 deletion cmd/kops/create_cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"github.com/golang/glog"
"github.com/spf13/cobra"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/kops"
"k8s.io/kops/cmd/kops/util"
Expand Down Expand Up @@ -124,6 +125,11 @@ type CreateClusterOptions struct {

// ConfigBase is the location where we will store the configuration, it defaults to the state store
ConfigBase string

// DryRun mode output a cluster manifest of Output type.
DryRun bool
// Output type during a DryRun
Output string
}

func (o *CreateClusterOptions) InitDefaults() {
Expand Down Expand Up @@ -190,6 +196,11 @@ var (
--project my-gce-project \
--image "ubuntu-os-cloud/ubuntu-1604-xenial-v20170202" \
--yes
# Create manifest for a cluster in AWS
kops create cluster --name=kubernetes-cluster.example.com \
--state=s3://kops-state-1234 --zones=eu-west-1a \
--node-count=2 --dry-run -oyaml

`))

create_cluster_short = i18n.T("Create a Kubernetes cluster.")
Expand Down Expand Up @@ -227,7 +238,7 @@ func NewCmdCreateCluster(f *util.Factory, out io.Writer) *cobra.Command {
}

cmd.Flags().BoolVar(&options.Yes, "yes", options.Yes, "Specify --yes to immediately create the cluster")
cmd.Flags().StringVar(&options.Target, "target", options.Target, "Target - direct, terraform, cloudformation")
cmd.Flags().StringVar(&options.Target, "target", options.Target, fmt.Sprintf("Valid targets: %s, %s, %s. Set this flag to %s if you want kops to generate terraform", cloudup.TargetDirect, cloudup.TargetTerraform, cloudup.TargetDirect, cloudup.TargetTerraform))
cmd.Flags().StringVar(&options.Models, "model", options.Models, "Models to apply (separate multiple models with commas)")

// Configuration / state location
Expand Down Expand Up @@ -297,6 +308,10 @@ func NewCmdCreateCluster(f *util.Factory, out io.Writer) *cobra.Command {

cmd.Flags().StringVar(&options.APILoadBalancerType, "api-loadbalancer-type", options.APILoadBalancerType, "Sets the API loadbalancer type to either 'public' or 'internal'")

// DryRun mode that will print YAML or JSON
cmd.Flags().BoolVar(&options.DryRun, "dry-run", options.DryRun, "If true, only print the object that would be sent, without sending it. This flag can be used to create a cluster YAML or JSON manifest.")
cmd.Flags().StringVarP(&options.Output, "output", "o", options.Output, "Ouput format. One of json|yaml. Used with the --dry-run flag.")

if featureflag.SpecOverrideFlag.Enabled() {
cmd.Flags().StringSliceVar(&options.Overrides, "override", options.Overrides, "Directly configure values in the spec")
}
Expand Down Expand Up @@ -326,6 +341,11 @@ func RunCreateCluster(f *util.Factory, out io.Writer, c *CreateClusterOptions) e
isDryrun = true
targetName = cloudup.TargetDryRun
}

if c.DryRun && c.Output == "" {
return fmt.Errorf("unable to execute --dry-run without setting --output")
}

clusterName := c.ClusterName
if clusterName == "" {
return fmt.Errorf("--name is required")
Expand Down Expand Up @@ -987,6 +1007,32 @@ func RunCreateCluster(f *util.Factory, out io.Writer, c *CreateClusterOptions) e
return err
}

if c.DryRun {
var obj []runtime.Object
obj = append(obj, cluster)

for _, group := range fullInstanceGroups {
// Cluster name is not populated, and we need it
group.ObjectMeta.Labels = make(map[string]string)
group.ObjectMeta.Labels[api.LabelClusterName] = cluster.ObjectMeta.Name
obj = append(obj, group)
}
switch c.Output {
case OutputYaml:
if err := fullOutputYAML(out, obj...); err != nil {
return fmt.Errorf("error writing cluster yaml to stdout: %v", err)
}
return nil
case OutputJSON:
if err := fullOutputJSON(out, obj...); err != nil {
return fmt.Errorf("error writing cluster json to stdout: %v", err)
}
return nil
default:
return fmt.Errorf("unsupported output type %q", c.Output)
}
}

// Note we perform as much validation as we can, before writing a bad config
err = registry.CreateClusterConfig(clientset, cluster, fullInstanceGroups)
if err != nil {
Expand All @@ -1010,6 +1056,7 @@ func RunCreateCluster(f *util.Factory, out io.Writer, c *CreateClusterOptions) e
}
}

// Can we acutally get to this if??
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean that targetName is always != "" ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is targetName ever == "". I think it is always set, and the if statement is not needed

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you're right, but separate PR probably

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you pull this comment out of this PR?

if targetName != "" {
if isDryrun {
fmt.Fprintf(out, "Previewing changes that will be made:\n\n")
Expand Down
37 changes: 37 additions & 0 deletions cmd/kops/create_ig.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ import (
type CreateInstanceGroupOptions struct {
Role string
Subnets []string
// DryRun mode output an ig manifest of Output type.
DryRun bool
// Output type during a DryRun
Output string
}

var (
Expand All @@ -52,6 +56,10 @@ var (
# Create an instancegroup for the k8s-cluster.example.com cluster.
kops create ig --name=k8s-cluster.example.com node-example \
--role node --subnet my-subnet-name

# Create a YAML manifest for an instancegroup for the k8s-cluster.example.com cluster.
kops create ig --name=k8s-cluster.example.com node-example \
--role node --subnet my-subnet-name --dry-run -oyaml
`))

create_ig_short = i18n.T(`Create an instancegroup.`)
Expand Down Expand Up @@ -85,6 +93,9 @@ func NewCmdCreateInstanceGroup(f *util.Factory, out io.Writer) *cobra.Command {

cmd.Flags().StringVar(&options.Role, "role", options.Role, "Type of instance group to create ("+strings.Join(allRoles, ",")+")")
cmd.Flags().StringSliceVar(&options.Subnets, "subnet", options.Subnets, "Subnets in which to create instance group")
// DryRun mode that will print YAML or JSON
cmd.Flags().BoolVar(&options.DryRun, "dry-run", options.DryRun, "If true, only print the object that would be sent, without sending it. This flag can be used to create a cluster YAML or JSON manifest.")
cmd.Flags().StringVarP(&options.Output, "output", "o", options.Output, "Ouput format. One of json|yaml")

return cmd
}
Expand Down Expand Up @@ -142,6 +153,32 @@ func RunCreateInstanceGroup(f *util.Factory, cmd *cobra.Command, args []string,
return err
}

if options.DryRun {

if options.Output == "" {
return fmt.Errorf("must set output flag; yaml or json")
}

// Cluster name is not populated, and we need it
ig.ObjectMeta.Labels = make(map[string]string)
ig.ObjectMeta.Labels[api.LabelClusterName] = cluster.ObjectMeta.Name

switch options.Output {
case OutputYaml:
if err := fullOutputYAML(out, ig); err != nil {
return fmt.Errorf("error writing cluster yaml to stdout: %v", err)
}
return nil
case OutputJSON:
if err := fullOutputJSON(out, ig); err != nil {
return fmt.Errorf("error writing cluster json to stdout: %v", err)
}
return nil
default:
return fmt.Errorf("unsupported output type %q", options.Output)
}
}

var (
edit = editor.NewDefaultEditor(editorEnvs)
)
Expand Down
41 changes: 16 additions & 25 deletions cmd/kops/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ limitations under the License.
package main

import (
"encoding/json"
"fmt"
"io"

Expand Down Expand Up @@ -153,35 +152,27 @@ func RunGet(context Factory, out io.Writer, options *GetOptions) error {
return err
}

switch options.output {
case OutputYaml:

err = clusterOutputYAML(clusters, out)
if err != nil {
return err
var obj []runtime.Object
if options.output != OutputTable {
obj = append(obj, cluster)
for _, group := range instancegroups {
obj = append(obj, group)
}
}

if err := writeYAMLSep(out); err != nil {
return err
switch options.output {
case OutputYaml:
if err := fullOutputYAML(out, obj...); err != nil {
return fmt.Errorf("error writing cluster yaml to stdout: %v", err)
}

err = igOutputYAML(instancegroups, out)
if err != nil {
return err
}
return nil

case OutputJSON:
return fmt.Errorf("not implemented")
// TODO this is not outputing valid json. Not sure what cluster and instance groups should look like
/*
err = clusterOutputJson(clusters,out)
if err != nil {
return err
}
err = igOutputJson(instancegroups,out)
if err != nil {
return err
}*/
if err := fullOutputJSON(out, obj...); err != nil {
return fmt.Errorf("error writing cluster json to stdout: %v", err)
}
return nil

case OutputTable:
fmt.Fprintf(os.Stdout, "Cluster\n")
Expand Down Expand Up @@ -235,7 +226,7 @@ func marshalYaml(obj runtime.Object) ([]byte, error) {

// obj must be a pointer to a marshalable object
func marshalJSON(obj runtime.Object) ([]byte, error) {
j, err := json.MarshalIndent(obj, "", " ")
j, err := kopscodecs.ToVersionedJSON(obj)
if err != nil {
return nil, fmt.Errorf("error marshaling json: %v", err)
}
Expand Down
54 changes: 42 additions & 12 deletions cmd/kops/get_cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@ package main

import (
"fmt"
"io"
"os"
"strings"

"io"

"github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/kops/cmd/kops/util"
api "k8s.io/kops/pkg/apis/kops"
Expand Down Expand Up @@ -133,7 +133,7 @@ func RunGetClusters(context Factory, out io.Writer, options *GetClusterOptions)
}

if len(clusters) == 0 {
return fmt.Errorf("No clusters found")
return fmt.Errorf("no clusters found")
}

if options.FullSpec {
Expand All @@ -146,14 +146,20 @@ func RunGetClusters(context Factory, out io.Writer, options *GetClusterOptions)
fmt.Fprint(out, get_cluster_full_warning)
}

var obj []runtime.Object
if options.output != OutputTable {
for _, c := range clusters {
obj = append(obj, c)
}
}

switch options.output {
case OutputTable:
return clusterOutputTable(clusters, out)
case OutputYaml:
return clusterOutputYAML(clusters, out)
return fullOutputYAML(out, obj...)
case OutputJSON:
return clusterOutputJson(clusters, out)

return fullOutputJSON(out, obj...)
default:
return fmt.Errorf("Unknown output format: %q", options.output)
}
Expand Down Expand Up @@ -206,23 +212,47 @@ func clusterOutputTable(clusters []*api.Cluster, out io.Writer) error {
return t.Render(clusters, out, "NAME", "CLOUD", "ZONES")
}

func clusterOutputJson(clusters []*api.Cluster, out io.Writer) error {
for _, cluster := range clusters {
if err := marshalToWriter(cluster, marshalJSON, out); err != nil {
// fullOutputJson outputs the marshalled JSON of a list of clusters and instance groups. It will handle
// nils for clusters and instanceGroups slices.
func fullOutputJSON(out io.Writer, args ...runtime.Object) error {
argsLen := len(args)

if argsLen > 1 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should always write the array, even if it is empty. I don't think an empty string is valid JSON?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JSON object is not an array. For instance when we create an ig, we just get a single object

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can always write an array, but at least with a single JSON object create -f works, with a JSON array it bombs

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We discussed this; you are right - better to only write the array if there are multiple members. This way a single object is an object

if _, err := fmt.Fprint(out, "["); err != nil {
return err
}
}

for i, arg := range args {
if i != 0 {
if _, err := fmt.Fprint(out, ","); err != nil {
return err
}
}
if err := marshalToWriter(arg, marshalJSON, out); err != nil {
return err
}
}

if argsLen > 1 {
if _, err := fmt.Fprint(out, "]"); err != nil {
return err
}
}

return nil
}

func clusterOutputYAML(clusters []*api.Cluster, out io.Writer) error {
for i, cluster := range clusters {
// fullOutputJson outputs the marshalled JSON of a list of clusters and instance groups. It will handle
// nils for clusters and instanceGroups slices.
func fullOutputYAML(out io.Writer, args ...runtime.Object) error {
for i, obj := range args {
if i != 0 {
if err := writeYAMLSep(out); err != nil {
return fmt.Errorf("error writing to stdout: %v", err)
}
}
if err := marshalToWriter(cluster, marshalYaml, out); err != nil {
if err := marshalToWriter(obj, marshalYaml, out); err != nil {
return err
}
}
Expand Down
Loading