Skip to content
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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.24.2

require (
github.com/spf13/cobra v1.9.1
k8s.io/api v0.33.1
k8s.io/apimachinery v0.33.1
)

Expand Down Expand Up @@ -40,7 +41,6 @@ require (
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.33.1 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
Expand Down
3 changes: 1 addition & 2 deletions pkg/cmd/copykeycloakadminpassword.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
ns "github.com/SwissDataScienceCenter/renku-dev-utils/pkg/namespace"
"github.com/spf13/cobra"
"golang.design/x/clipboard"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

var copyKeycloakAdminPasswordCmd = &cobra.Command{
Expand Down Expand Up @@ -42,7 +41,7 @@ func runCopyKeycloakAdminPassword(cmd *cobra.Command, args []string) {
os.Exit(1)
}

secret, err := clients.CoreV1().Secrets(namespace).Get(ctx, secretName, metav1.GetOptions{})
secret, err := k8s.GetSecret(ctx, clients, namespace, secretName)
if err != nil {
fmt.Println(err)
os.Exit(1)
Expand Down
128 changes: 128 additions & 0 deletions pkg/cmd/makemeadmin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package cmd

import (
"context"
"fmt"
"os"

"github.com/SwissDataScienceCenter/renku-dev-utils/pkg/git"
"github.com/SwissDataScienceCenter/renku-dev-utils/pkg/github"
"github.com/SwissDataScienceCenter/renku-dev-utils/pkg/k8s"
"github.com/SwissDataScienceCenter/renku-dev-utils/pkg/keycloak"
ns "github.com/SwissDataScienceCenter/renku-dev-utils/pkg/namespace"
"github.com/spf13/cobra"
)

var makeMeAdminCmd = &cobra.Command{
Use: "make-me-admin",
Aliases: []string{"mma"},
Short: "Makes you admin of the current deployment",
Run: makeMeAdmin,
}

func makeMeAdmin(cmd *cobra.Command, args []string) {
ctx := context.Background()

if userEmail == "" {
gitCli, err := git.NewGitCLI("")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
userEmail, err = gitCli.GetUserEmail(ctx)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
}

if namespace == "" {
cli, err := github.NewGitHubCLI("")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
namespace, err = ns.FindCurrentNamespace(ctx, cli)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
}

clients, err := k8s.GetClientset()
if err != nil {
fmt.Println(err)
os.Exit(1)
}

secret, err := k8s.GetSecret(ctx, clients, namespace, secretName)
if err != nil {
fmt.Println(err)
os.Exit(1)
}

username, found := secret.Data[secretKeyUsername]
if !found {
fmt.Printf("The secret did not contain '%s'\n", secretKeyUsername)
os.Exit(1)
}

password, found := secret.Data[secretKey]
if !found {
fmt.Printf("The secret did not contain '%s'\n", secretKey)
os.Exit(1)
}

deploymentURL, err := ns.GetDeploymentURL(namespace)
if err != nil {
fmt.Println(err)
os.Exit(1)
}

kcURL := deploymentURL.JoinPath("./auth")
kcClient, err := keycloak.NewKeycloakClient(kcURL.String())
if err != nil {
fmt.Println(err)
os.Exit(1)
}

err = kcClient.Authenticate(ctx, string(username), string(password))
if err != nil {
fmt.Println(err)
os.Exit(1)
}

userID, err := kcClient.FindUser(ctx, renkuRealm, userEmail)
if err != nil {
fmt.Println(err)
os.Exit(1)
}

isAdmin, err := kcClient.IsRenkuAdmin(ctx, renkuRealm, userID)
if err != nil {
fmt.Println(err)
os.Exit(1)
}

if isAdmin {
fmt.Printf("User '%s' is already a renku admin\n", userEmail)
os.Exit(0)
}

err = kcClient.AddRenkuAdminRoleToUser(ctx, renkuRealm, userID)
if err != nil {
fmt.Println(err)
os.Exit(1)
}

fmt.Println("Done, you are now a Renku admin!")
}

func init() {
makeMeAdminCmd.Flags().StringVarP(&namespace, "namespace", "n", "", "k8s namespace")
makeMeAdminCmd.Flags().StringVar(&secretName, "secret-name", "keycloak-password-secret", "secret name")
makeMeAdminCmd.Flags().StringVar(&secretKey, "secret-key", "KEYCLOAK_ADMIN_PASSWORD", "secret key")
makeMeAdminCmd.Flags().StringVar(&secretKeyUsername, "secret-key-username", "KEYCLOAK_ADMIN", "secret key for the admin username")
makeMeAdminCmd.Flags().StringVar(&renkuRealm, "renku-realm", "Renku", "the Keycloak realm used by renku")
makeMeAdminCmd.Flags().StringVarP(&userEmail, "user-email", "u", "", "your email")
}
4 changes: 1 addition & 3 deletions pkg/cmd/opendeployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package cmd
import (
"context"
"fmt"
"net/url"
"os"
"os/exec"
"runtime"
Expand Down Expand Up @@ -36,8 +35,7 @@ func openDeployment(cmd *cobra.Command, args []string) {
}
}

// TODO: Can we derive the URL by inspecting ingresses in the k8s namespace?
openURL, err := url.Parse(fmt.Sprintf("https://%s.dev.renku.ch", namespace))
openURL, err := ns.GetDeploymentURL(namespace)
if err != nil {
fmt.Println(err)
os.Exit(1)
Expand Down
6 changes: 5 additions & 1 deletion pkg/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@ import (

var deleteNamespace bool
var namespace string
var secretName string
var renkuRealm string
var secretKey string
var secretKeyUsername string
var secretName string
var userEmail string

var rootCmd = &cobra.Command{
Use: "rdu",
Expand All @@ -30,6 +33,7 @@ func runRoot(cmd *cobra.Command, args []string) error {
func init() {
rootCmd.AddCommand(cleanupDeploymentCmd)
rootCmd.AddCommand(copyKeycloakAdminPasswordCmd)
rootCmd.AddCommand(makeMeAdminCmd)
rootCmd.AddCommand(openDeploymentCmd)
rootCmd.AddCommand(versionCmd)
}
Expand Down
33 changes: 33 additions & 0 deletions pkg/git/cli.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package git

import (
"context"
"fmt"
"os/exec"

"github.com/SwissDataScienceCenter/renku-dev-utils/pkg/executils"
)

type GitCLI struct {
git string
}

func NewGitCLI(git string) (*GitCLI, error) {
if git == "" {
git = "git"
}

path, err := exec.LookPath(git)
if err != nil {
return nil, err
}

fmt.Printf("Found git: %s", path)
fmt.Println()
return &GitCLI{git: git}, nil
}

func (cli *GitCLI) RunCmd(ctx context.Context, arg ...string) ([]byte, error) {
cmd := exec.CommandContext(ctx, cli.git, arg...)
return executils.FormatOutput(cmd.Output())
}
18 changes: 18 additions & 0 deletions pkg/git/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package git

import (
"context"
"fmt"
"strings"
)

func (cli *GitCLI) GetUserEmail(ctx context.Context) (email string, err error) {
out, err := cli.RunCmd(ctx, "config", "--get", "user.email")
if err != nil {
return "", err
}

email = strings.TrimSpace(string(out))
fmt.Printf("Detected user email: %s\n", email)
return email, nil
}
13 changes: 13 additions & 0 deletions pkg/k8s/secret.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package k8s

import (
"context"

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
)

func GetSecret(ctx context.Context, clients *kubernetes.Clientset, namespace string, secretName string) (secret *corev1.Secret, err error) {
return clients.CoreV1().Secrets(namespace).Get(ctx, secretName, metav1.GetOptions{})
}
111 changes: 111 additions & 0 deletions pkg/keycloak/admin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package keycloak

import (
"context"
"fmt"
"net/url"
)

const renkuAdminRole string = "renku-admin"

func (client *KeycloakClient) FindUser(ctx context.Context, realm string, email string) (userID string, err error) {
getURL := client.GetAdminUsersURL(realm)

query := url.Values{}
query.Set("email", email)
query.Set("exact", "true")
getURL.RawQuery = query.Encode()

var result []getUsersResponse
_, err = client.GetJSON(ctx, getURL.String(), &result)
if err != nil {
return "", err
}

for _, user := range result {
if user.Email == email {
userID = user.ID
fmt.Printf("Found user ID: %s\n", userID)
return userID, nil
}
}

return "", fmt.Errorf("Could not find user '%s' in Keycloak", email)
}

func (client *KeycloakClient) GetAdminUsersURL(realm string) *url.URL {
path := fmt.Sprintf("./admin/realms/%s/users", realm)
return client.BaseURL.JoinPath(path)
}

type getUsersResponse struct {
ID string `json:"id"`
Email string `json:"email"`
}

func (client *KeycloakClient) IsRenkuAdmin(ctx context.Context, realm string, userID string) (isAdmin bool, err error) {
getURL := client.GetAdminRolesURL(realm, userID)

var result []roleMapping
_, err = client.GetJSON(ctx, getURL.String(), &result)
if err != nil {
return false, err
}

for _, role := range result {
if role.Name == renkuAdminRole {
return true, nil
}
}
return false, nil
}

func (client *KeycloakClient) findRenkuAdminRole(ctx context.Context, realm string, userID string) (role roleMapping, err error) {
getURL := client.GetAdminAvailavleRolesURL(realm, userID)

var result []roleMapping
_, err = client.GetJSON(ctx, getURL.String(), &result)
if err != nil {
return roleMapping{}, err
}

for _, roleObj := range result {
if roleObj.Name == renkuAdminRole {
role = roleObj
return role, err
}
}
return roleMapping{}, fmt.Errorf("Could not find role '%s' in Keycloak", renkuAdminRole)
}

func (client *KeycloakClient) AddRenkuAdminRoleToUser(ctx context.Context, realm string, userID string) error {
role, err := client.findRenkuAdminRole(ctx, realm, userID)

postURL := client.GetAdminRolesURL(realm, userID)

body := []roleMapping{
role,
}

_, err = client.PostJSON(ctx, postURL.String(), body, nil)
if err != nil {
return err
}
return nil
}

func (client *KeycloakClient) GetAdminRolesURL(realm string, userID string) *url.URL {
path := fmt.Sprintf("./admin/realms/%s/users/%s/role-mappings/realm", realm, userID)
return client.BaseURL.JoinPath(path)
}

func (client *KeycloakClient) GetAdminAvailavleRolesURL(realm string, userID string) *url.URL {
path := fmt.Sprintf("./admin/realms/%s/users/%s/role-mappings/realm/available", realm, userID)
return client.BaseURL.JoinPath(path)
}

type roleMapping struct {
ID string `json:"id,omitempty"`
ContainerID string `json:"containerId,omitempty"`
Name string `json:"name,omitempty"`
}
Loading