Skip to content

Commit

Permalink
Merge "CLI: add Zuul subcommands"
Browse files Browse the repository at this point in the history
  • Loading branch information
Microzuul CI authored and Gerrit Code Review committed Feb 8, 2024
2 parents 9fcdd1c + dd79e9f commit c5423eb
Show file tree
Hide file tree
Showing 8 changed files with 396 additions and 44 deletions.
86 changes: 86 additions & 0 deletions cli/cmd/zuul/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
Copyright © 2024 Red Hat
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 zuul

import (
"errors"
"fmt"
"os"
"strings"

cliutils "github.com/softwarefactory-project/sf-operator/cli/cmd/utils"
"github.com/spf13/cobra"
ctrl "sigs.k8s.io/controller-runtime"
)

var (
zuulGetAllowedArgs = []string{"client-config", "auth-token"}
)

func zuulCreate(kmd *cobra.Command, args []string) {
cliCtx := cliutils.GetCLIctxOrDie(kmd, args, zuulGetAllowedArgs)
target := args[0]
ns := cliCtx.Namespace
kubeContext := cliCtx.KubeContext
authConfig, _ := kmd.Flags().GetString("auth-config")
tenant, _ := kmd.Flags().GetString("tenant")
user, _ := kmd.Flags().GetString("user")
expiry, _ := kmd.Flags().GetInt("expires-in")
if target == "auth-token" {
if tenant == "" {
ctrl.Log.Error(errors.New("missing argument"), "A tenant is required")
os.Exit(1)
}
token := CreateAuthToken(kubeContext, ns, authConfig, tenant, user, expiry)
fmt.Println(token)
}
if target == "client-config" {
insecure, _ := kmd.Flags().GetBool("insecure")
fqdn := cliCtx.FQDN
config := CreateClientConfig(kubeContext, ns, fqdn, authConfig, tenant, user, expiry, !insecure)
fmt.Println(config)
}
}

func MkZuulCmd() *cobra.Command {
var (
authConfig string
tenant string
user string
expiry int
insecure bool
zuulCmd = &cobra.Command{
Use: "zuul",
Short: "Zuul subcommands",
Long: "These subcommands can be used to interact with the Zuul component of a Software Factory deployment",
}
createCmd, _, _ = cliutils.GetCRUDSubcommands()
)

createCmd.Run = zuulCreate
createCmd.Use = "create {" + strings.Join(zuulGetAllowedArgs, ", ") + "}"
createCmd.Long = "Create a Zuul resource: an authentication token or a CLI configuration file"
createCmd.ValidArgs = zuulGetAllowedArgs
createCmd.Flags().StringVar(&authConfig, "auth-config", "zuul_client", "the local authentication config to use to generate a token")
createCmd.Flags().StringVar(&tenant, "tenant", "", "the tenant on which the token should grant admin access")
createCmd.Flags().StringVar(&user, "user", "John Doe", "a username, only used for audit purposes in Zuul's access logs")
createCmd.Flags().IntVar(&expiry, "expires-in", 3600, "how long the authentication token should be valid for")
createCmd.Flags().BoolVar(&insecure, "insecure", false, "do not verify SSL certificates when connection to Zuul")

zuulCmd.AddCommand(createCmd)
return zuulCmd
}
136 changes: 136 additions & 0 deletions cli/cmd/zuul/utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/*
Copyright © 2024 Red Hat
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 zuul deals with zuul-related subcommands.
package zuul

import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"strings"

"github.com/softwarefactory-project/sf-operator/controllers"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/remotecommand"
"k8s.io/kubectl/pkg/scheme"

ctrl "sigs.k8s.io/controller-runtime"
)

type ZuulAPITenant struct {
Name string `json:"name"`
Projects int `json:"projects"`
Queue int `json:"queue"`
}

func GetTenants(fqdn string, verify bool) []string {
tr := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: !verify,
},
}
client := &http.Client{Transport: tr}
tenantsURL := "https://" + fqdn + "/zuul/api/tenants"
resp, err := client.Get(tenantsURL)
if err != nil {
ctrl.Log.Error(err, "HTTP protocol error")
os.Exit(1)
}
if resp.StatusCode >= 400 {
ctrl.Log.Error(errors.New("bad status"), "API returned status "+resp.Status)
os.Exit(1)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
ctrl.Log.Error(err, "Error reading API response")
os.Exit(1)
}
_tenants := []ZuulAPITenant{}
err = json.Unmarshal(body, &_tenants)
if err != nil {
ctrl.Log.Error(err, "Error marshalling JSON response")
os.Exit(1)
}
tenants := []string{}
for _, tenant := range _tenants {
tenants = append(tenants, tenant.Name)
}
return tenants
}

func getClientset(kubeContext string) (*rest.Config, *kubernetes.Clientset) {
restConfig := controllers.GetConfigContextOrDie(kubeContext)
kubeClientset, err := kubernetes.NewForConfig(restConfig)
if err != nil {
ctrl.Log.Error(err, "Could not instantiate Clientset")
os.Exit(1)
}
return restConfig, kubeClientset
}

func getFirstPod(prefix string, namespace string, kubeContext string) *v1.Pod {
var ctr *v1.Pod = nil

_, kubeClientset := getClientset(kubeContext)

podslist, _ := kubeClientset.CoreV1().Pods(namespace).List(context.TODO(), metav1.ListOptions{})
for _, container := range podslist.Items {
if strings.HasPrefix(container.Name, prefix) {
ctr = &container
break
}
}
return ctr
}

func runRemoteCmd(kubeContext string, namespace string, podName string, containerName string, cmdArgs []string) *bytes.Buffer {
restConfig, kubeClientset := getClientset(kubeContext)
buffer := &bytes.Buffer{}
errorBuffer := &bytes.Buffer{}
request := kubeClientset.CoreV1().RESTClient().Post().Resource("Pods").Namespace(namespace).Name(podName).SubResource("exec").VersionedParams(
&v1.PodExecOptions{
Container: containerName,
Command: cmdArgs,
Stdin: false,
Stdout: true,
Stderr: true,
},
scheme.ParameterCodec,
)
exec, _ := remotecommand.NewSPDYExecutor(restConfig, "POST", request.URL())
err := exec.StreamWithContext(context.Background(), remotecommand.StreamOptions{
Stdout: buffer,
Stderr: errorBuffer,
})
if err != nil {
errMsg := fmt.Sprintf("Command \"%s\" [Pod: %s - Container: %s] failed with the following stderr: %s",
strings.Join(cmdArgs, " "), podName, containerName, errorBuffer.String())
ctrl.Log.Error(err, errMsg)
os.Exit(1)
}
return buffer
}
39 changes: 39 additions & 0 deletions cli/cmd/zuul/zuul-admin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
Copyright © 2024 Red Hat
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 zuul

import "strconv"

// zuul-admin proxy commands.

func CreateAuthToken(kubeContext string, namespace string, authConfig string, tenant string, user string, expiry int) string {
_authConfig := authConfig
if _authConfig == "" {
_authConfig = "zuul_client"
}
scheduler := getFirstPod("zuul-scheduler", namespace, kubeContext)
createAuthTokenCmd := []string{
"zuul-admin",
"create-auth-token",
"--auth-config", _authConfig,
"--tenant", tenant,
"--user", user,
"--expires-in", strconv.Itoa(expiry),
}
token := runRemoteCmd(kubeContext, namespace, scheduler.Name, "zuul-scheduler", createAuthTokenCmd)
return token.String()
}
75 changes: 75 additions & 0 deletions cli/cmd/zuul/zuul-client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
Copyright © 2024 Red Hat
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 zuul

import (
"bytes"
"os"
"strings"
"text/template"

ctrl "sigs.k8s.io/controller-runtime"
)

type ZuulClientConfigSection struct {
Name string
URL string
Tenant string
VerifySSL bool
AuthToken string
}

var configTemplate = `
[{{ .Name }}]
url={{ .URL }}
{{ if .Tenant }}tenant={{ .Tenant }}{{ else }}{{ end }}
verify_ssl={{ if .VerifySSL }}True{{ else }}False{{ end }}
{{ if .AuthToken }}auth_token={{ .AuthToken }}{{ else }}{{ end }}
`

func CreateClientConfig(kubeContext string, namespace string, fqdn string, authConfig string, tenant string, user string, expiry int, verify bool) string {
var tenants []string
if tenant != "" {
tenants = []string{tenant}
} else {
tenants = GetTenants(fqdn, verify)
}
var config string
for _, t := range tenants {
token := CreateAuthToken(kubeContext, namespace, authConfig, t, user, expiry)
section := ZuulClientConfigSection{
Name: t,
URL: "https://" + fqdn + "/zuul",
Tenant: t,
VerifySSL: verify,
AuthToken: strings.TrimPrefix(token, "Bearer "),
}
confTemplate, err := template.New("configSection").Parse(configTemplate)
if err != nil {
ctrl.Log.Error(err, "Error initializing config template")
os.Exit(1)
}
var buf bytes.Buffer
if err := confTemplate.Execute(&buf, section); err != nil {
ctrl.Log.Error(err, "Error applying config template")
os.Exit(1)
}
config += buf.String()
}
return config
}
46 changes: 45 additions & 1 deletion doc/reference/cli/main.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ deployments, beyond what can be defined in a custom resource manifest.
1. [configure TLS](#configure-tls)
1. [restore](#restore)
1. [wipe](#wipe)
1. [Zuul](#zuul)
1. [create auth-token](#create-auth-token)
1. [create client-config](#create-client-config)

## Running the CLI

Expand Down Expand Up @@ -450,4 +453,45 @@ Flags:
| Argument | Type | Description | Optional | Default |
|----------|------|-------|----|----|
| --rm-data | boolean | Also delete all persistent volumes after removing the instances | yes | False |
| --all | boolean | Remove all data like with the `--rm-data` flag, and remove the operator from the cluster | yes | False |
| --all | boolean | Remove all data like with the `--rm-data` flag, and remove the operator from the cluster | yes | False |

### Zuul

These subcommands can be used to interact with the Zuul component of a deployment.

#### create auth-token

The `create auth-token` subcommand can be used to create a custom authentication token that can be used with the [zuul-client CLI utility](https://zuul-ci.org/docs/zuul-client/).

```sh
go run ./main.go [GLOBAL FLAGS] zuul create auth-token [FLAGS]
```

Flags:

| Argument | Type | Description | Optional | Default |
|----------|------|-------|----|----|
| --auth-config | string | The authentication configuration to use | yes | zuul_client |
| --tenant | string | The tenant on which to grant admin access | no | - |
| --user | string | a username, used for audit purposes in Zuul's access logs | yes | "John Doe" |
| --expiry | int | How long in seconds the authentication token should be valid for | yes | 3600 |

#### create client-config

The `create client-config` generates a configuration file that can be used with the [zuul-client CLI utility](https://zuul-ci.org/docs/zuul-client/) against the Software Factory deployment.

> ⚠️ The command provisions authentication tokens that grant admin access to all tenants. It is recommended to review and eventually edit the output of the command before forwarding it to third parties.

```sh
go run ./main.go [GLOBAL FLAGS] zuul create client-config [FLAGS] > zuul-client.conf
```

Flags:

| Argument | Type | Description | Optional | Default |
|----------|------|-------|----|----|
| --auth-config | string | The authentication configuration to use | yes | zuul_client |
| --tenant | string | The tenant on which to grant admin access | no | - |
| --user | string | a username, used for audit purposes in Zuul's access logs | yes | "John Doe" |
| --expiry | int | How long in seconds the authentication token should be valid for | yes | 3600 |
| --insecure | boolean | skip SSL validation when connecting to Zuul | yes | False |
Loading

0 comments on commit c5423eb

Please sign in to comment.