Skip to content

Commit

Permalink
Add CRUD support for spaces groups (#466)
Browse files Browse the repository at this point in the history
* Add CRUD for space groups

* Consolidate get profile logic

* Add GetCurrentContext and error messages
  • Loading branch information
RedbackThomson committed Apr 18, 2024
1 parent 5804ff1 commit f5a4c2f
Show file tree
Hide file tree
Showing 8 changed files with 443 additions and 0 deletions.
115 changes: 115 additions & 0 deletions cmd/up/group/cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// Copyright 2024 Upbound Inc
//
// 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 group

import (
"context"
"strconv"

"github.com/alecthomas/kong"
"github.com/crossplane/crossplane-runtime/pkg/errors"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/client-go/kubernetes/scheme"

spacesv1beta1 "github.com/upbound/up-sdk-go/apis/spaces/v1beta1"
"github.com/upbound/up/internal/feature"
"github.com/upbound/up/internal/profile"
"github.com/upbound/up/internal/upbound"
)

var (
fieldNames = []string{"NAME", "PROTECTED"}
)

func init() {
runtime.Must(spacesv1beta1.AddToScheme(scheme.Scheme))
}

// BeforeReset is the first hook to run.
func (c *Cmd) BeforeReset(p *kong.Path, maturity feature.Maturity) error {
return feature.HideMaturity(p, maturity)
}

// AfterApply constructs and binds an Upbound context to any subcommands
// that have Run() methods that receive it.
func (c *Cmd) AfterApply(kongCtx *kong.Context) error {
upCtx, err := upbound.NewFromFlags(c.Flags)
if err != nil {
return err
}
kongCtx.Bind(upCtx)

if !upCtx.Profile.IsSpace() {
// TODO: add legacy support
return errors.New("Only Spaces contexts supported for now.")
}
return nil
}

// Cmd contains commands for interacting with groups.
type Cmd struct {
Create createCmd `cmd:"" help:"Create a group."`
Delete deleteCmd `cmd:"" help:"Delete a group."`
List listCmd `cmd:"" help:"List groups in the space."`
Get getCmd `cmd:"" help:"Get a group."`

// Common Upbound API configuration
Flags upbound.Flags `embed:""`
}

func (c *Cmd) Help() string {
return `
Interact with groups within the current space. Both Upbound profiles and
local Spaces are supported. Use the "profile" management command to switch
between different Upbound profiles or to connect to a local Space.`
}

func extractGroupFields(obj any) []string {
resp, ok := obj.(corev1.Namespace)
if !ok {
return []string{"unknown", "unknown"}
}

protected := false
if av, ok := resp.ObjectMeta.Labels[spacesv1beta1.ControlPlaneGroupProtectionKey]; ok {
if val, err := strconv.ParseBool(av); err == nil {
protected = val
}
}

return []string{
resp.GetObjectMeta().GetName(),
strconv.FormatBool(protected),
}
}

func getCurrentProfile(ctx context.Context, upCtx *upbound.Context) (*profile.Profile, error) {
// get context
_, currentProfile, ctp, err := upCtx.Cfg.GetCurrentContext(ctx)
if err != nil {
return nil, err
}
if currentProfile == nil {
return nil, errors.New(profile.NoSpacesContextMsg)
}
if ctp.Namespace == "" {
return nil, errors.New(profile.NoGroupMsg)
}
if ctp.Name != "" {
return nil, errors.New("Cannot list control planes from inside a control plane, use `up ctx ..` to switch to a group level.")
}
return currentProfile, nil
}
72 changes: 72 additions & 0 deletions cmd/up/group/create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright 2024 Upbound Inc
//
// 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 group

import (
"context"
"strconv"

"github.com/pterm/pterm"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"

spacesv1beta1 "github.com/upbound/up-sdk-go/apis/spaces/v1beta1"
"github.com/upbound/up/internal/upbound"
"github.com/upbound/up/internal/upterm"
)

// createCmd creates a group in a space.
type createCmd struct {
Name string `arg:"" required:"" help:"Name of group."`
DeletionProtection bool `name:"deletion-protection" optional:"" default:"false" help:"Enable deletion protection on the new group." negatable:""`
}

// Run executes the create command.
func (c *createCmd) Run(ctx context.Context, printer upterm.ObjectPrinter, upCtx *upbound.Context, p pterm.TextPrinter) error { // nolint:gocyclo
// get profile
currentProfile, err := getCurrentProfile(ctx, upCtx)
if err != nil {
return err
}

// create client
restConfig, _, err := currentProfile.GetSpaceRestConfig()
if err != nil {
return err
}
cl, err := ctrlclient.New(restConfig, ctrlclient.Options{})
if err != nil {
return err
}

// create group
group := corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: c.Name,
Labels: map[string]string{
spacesv1beta1.ControlPlaneGroupLabelKey: "true",
spacesv1beta1.ControlPlaneGroupProtectionKey: strconv.FormatBool(c.DeletionProtection),
},
},
}

if err := cl.Create(ctx, &group); err != nil {
return err
}

p.Printfln("%s created", c.Name)
return nil
}
84 changes: 84 additions & 0 deletions cmd/up/group/delete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Copyright 2024 Upbound Inc
//
// 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 group

import (
"context"
"strconv"

"github.com/pterm/pterm"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"

"github.com/crossplane/crossplane-runtime/pkg/errors"

spacesv1beta1 "github.com/upbound/up-sdk-go/apis/spaces/v1beta1"
"github.com/upbound/up/internal/upbound"
"github.com/upbound/up/internal/upterm"
)

// deleteCmd creates a group in a space.
type deleteCmd struct {
Name string `arg:"" required:"" help:"Name of group."`
Force bool `name:"force" optional:"" default:"false" help:"Force the deletion of the group."`
}

// Run executes the create command.
func (c *deleteCmd) Run(ctx context.Context, printer upterm.ObjectPrinter, upCtx *upbound.Context, p pterm.TextPrinter) error { // nolint:gocyclo
// get profile
currentProfile, err := getCurrentProfile(ctx, upCtx)
if err != nil {
return err
}

// create client
restConfig, _, err := currentProfile.GetSpaceRestConfig()
if err != nil {
return err
}
cl, err := ctrlclient.New(restConfig, ctrlclient.Options{})
if err != nil {
return err
}

// delete group
group := corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: c.Name,
},
}

// ensure deletion protection is disabled, if not forcing
if !c.Force {
if err := cl.Get(ctx, types.NamespacedName{Name: c.Name}, &group); err != nil {
return err
}

if protEn, err := strconv.ParseBool(group.Labels[spacesv1beta1.ControlPlaneGroupProtectionKey]); err != nil {
return err
} else if protEn {
return errors.New("Deletion protection is enabled on the specified group. Use '--force' to delete anyway.")
}
}

if err := cl.Delete(ctx, &group); err != nil {
return err
}

p.Printfln("%s deleted", c.Name)
return nil
}
74 changes: 74 additions & 0 deletions cmd/up/group/get.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright 2024 Upbound Inc
//
// 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 group

import (
"context"
"fmt"

"github.com/alecthomas/kong"
"github.com/pterm/pterm"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"

spacesv1beta1 "github.com/upbound/up-sdk-go/apis/spaces/v1beta1"
"github.com/upbound/up/internal/upbound"
"github.com/upbound/up/internal/upterm"
)

// getCmd gets a specific group in a space.
type getCmd struct {
Name string `arg:"" required:"" help:"Name of group."`
}

// AfterApply sets default values in command after assignment and validation.
func (c *getCmd) AfterApply(kongCtx *kong.Context) error {
kongCtx.Bind(pterm.DefaultTable.WithWriter(kongCtx.Stdout).WithSeparator(" "))

return nil
}

// Run executes the list command.
func (c *getCmd) Run(ctx context.Context, printer upterm.ObjectPrinter, upCtx *upbound.Context, p pterm.TextPrinter) error { // nolint:gocyclo
// get profile
currentProfile, err := getCurrentProfile(ctx, upCtx)
if err != nil {
return err
}

// create client
restConfig, _, err := currentProfile.GetSpaceRestConfig()
if err != nil {
return err
}
cl, err := ctrlclient.New(restConfig, ctrlclient.Options{})
if err != nil {
return err
}

// list groups
var ns corev1.Namespace
if err := cl.Get(ctx, types.NamespacedName{Name: c.Name}, &ns); err != nil {
return err
}

// only print the group if it is a registered group
if _, ok := ns.Labels[spacesv1beta1.ControlPlaneGroupLabelKey]; !ok {
return fmt.Errorf("namespace %q is not a group", c.Name)
}

return printer.Print(ns, fieldNames, extractGroupFields)
}
Loading

0 comments on commit f5a4c2f

Please sign in to comment.