Skip to content

Commit

Permalink
initial import of opfcli command line tool
Browse files Browse the repository at this point in the history
This is my attempt at implementing what we talked about in
operate-first/SRE#299. It's an integrated command line tool written in
Go that currently provides equivalents for:

- onboarding.sh (called create-project)
- enable-monitoring.sh
- operate-first/apps#626 (called grant-access)

This is my first time writing Go, ever, so there may be lots of
non-idiomatic code. In your reviews, be kind, but be thorough smile.
  • Loading branch information
larsks committed May 17, 2021
1 parent 5e82ac1 commit 1c6bd0e
Show file tree
Hide file tree
Showing 26 changed files with 1,350 additions and 17 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
opfcli
172 changes: 155 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,33 +1,171 @@
# Operate First template for repositories
# opfcli

Derive new repositories from this template
## Usage

List of featurese:
```
A command line tool for Operate First GitOps.
## License
Use opfcli to interact with an Operate First style Kubernetes
configuration repository.
This template ensures new repos are created compliant with [ADR 0001](https://www.operate-first.cloud/blueprints/blueprint/docs/adr/0001-use-gpl3-as-license.md) and use GNU GPL v3 license.
Usage:
opfcli [command]
## AI-CoE CI Github application
Available Commands:
create-group Create a group
create-project Onboard a new project into Operate First
enable-monitoring Enable monitoring for a Kubernetes namespace
grant-access Grant a group access to a namespace
help Help about any command
AI-CoE CI provides easy and quick integration for build pipelines and checks for pull requests.
Flags:
-a, --app-name string application name (default "cluster-scope")
-f, --config string configuration file
-h, --help help for opfcli
-R, --repodir string path to opf repository
```

An empty [`.aicoe-ci.yaml`](.aicoe-ci.yaml) is created here, disabling all checks via this CI provider by default. Documentation can be found [here](https://github.com/AICoE/aicoe-ci/).
### create-group

## Prow CI
```
Create a group.
Prow is a CI provider developed for Kubernetes needs. Provides chat-ops management of pull requests, issues and declarative management for labels, branches and many more.
Create the group resource and associated kustomization file
We host our own deployment of Prow in Operate First available at [https://prow.operate-first.cloud/](https://prow.operate-first.cloud/).
Usage:
opfcli create-group group [flags]
Supported commands are listed [here](https://prow.operate-first.cloud/command-help). We have also enabled Prow to consume on-repository configuration files. You can specify your config in [`.prow.yaml`](.prow.yaml). Additional centralized configuration can be found in the [thoth-application repository](https://github.com/thoth-station/thoth-application/tree/master/prow/overlays/cnv-prod).
Flags:
-h, --help help for create-project
```

## Pre-commit
### create-project

By extension to Prow, we define a default pre-commit config for new repositories. Default hook configuration can be found in [`.pre-commit-config.yaml`](.pre-commit-config.yaml). Pre-commit is executed via Prow, see [`.prow.yaml`](.prow.yaml) for details.
```
Onboard a new project into Operate First.
We enable yamllint hook by default, since most of our repositories use yaml files extensively. Default configuration for this hook is located at [`yamllint-config.yaml`](yamllint-config.yaml).
- Register a new group
- Register a new namespace with appropriate role bindings for your group
To install and enable pre-commit locally please follow the instructions [here](https://pre-commit.com/#quick-start).
Usage:
opfcli create-project projectName projectOwner [flags]
It is advised for all contributors to enable pre-commit git hook via `pre-commit install` after cloning any repo within Operate First.
Flags:
-d, --description string Team description
-h, --help help for create-project
```

## enable-monitoring

```
Enable monitoring fora Kubernetes namespace.
This will add a RoleBinding to the target namespace that permits
Prometheus to access certain metrics about pods, services, etc.
Usage:
opfcli enable-monitoring [flags]
Flags:
-h, --help help for enable-monitoring
```

### grant-access

```
Grant a group acecss to a namespace.
Grant a group access to a namespace with the specifed role
(admin, edit, or view).
Usage:
opfcli grant-access namespace group role [flags]
Flags:
-h, --help help for grant-access
```

Use "opfcli [command] --help" for more information about a command.

## Configuration

The `opfcli` command will look for a configuration file `.opfcli.yaml`
in two places:

- It first checks in the top level of the current git repository. If
you are running the `opfcli` command outside of a git repository it
will instead check the current directory.

- If it doesn't find a local configuration file, it will look for
`~/.opfcli.yaml`.

### Available configuration options

- `app-name` -- sets the name of the directory containing your YAML
resources. This defaults to `cluster-scope`.

## Examples

### Create a project

```
opfcli create-project project1 group1 -d "This is project1"
```

This will result in:

```
cluster-scope/
├── base
│   ├── core
│   │   └── namespaces
│   │   └── project1
│   │   ├── kustomization.yaml
│   │   └── namespace.yaml
│   └── user.openshift.io
│   └── groups
│   └── group1
│   ├── group.yaml
│   └── kustomization.yaml
└── components
└── project-admin-rolebindings
└── group1
├── kustomization.yaml
└── rbac.yaml
```

### Create a group

```
opfcli create-group group2
```

This will result in:

```
cluster-scope/
└── base
└── user.openshift.io
└── groups
└── group1
├── group.yaml
└── kustomization.yaml
```

### Grant access to a project

```
opfcli grant-access project1 group2 view
```

This will result in:

```
cluster-scope/components/project-view-rolebindings/
└── group2
├── kustomization.yaml
└── rbac.yaml
```

(And will modify
`cluster-scope/base/core/namespaces/project1/kustomization.yaml`)
140 changes: 140 additions & 0 deletions cmd/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package cmd

import (
"fmt"
"io/ioutil"
"os"
"path/filepath"

"github.com/operate-first/opfcli/models"
"github.com/operate-first/opfcli/utils"
log "github.com/sirupsen/logrus"
)

func createNamespace(projectName, projectOwner, projectDescription string) {
appName := config.GetString("app-name")
path := filepath.Join(repoDirectory, appName, namespacePath, projectName, "namespace.yaml")

if utils.PathExists(filepath.Dir(path)) {
log.Fatalf("namespace %s already exists", projectName)
}

ns := models.NewNamespace(projectName, projectOwner, projectDescription)
nsOut := models.ToYAML(ns)

log.Printf("writing namespace definition to %s", filepath.Dir(path))
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
log.Fatalf("failed to create namespace directory: %v", err)
}

err := ioutil.WriteFile(path, nsOut, 0644)
if err != nil {
log.Fatalf("failed to write namespace file: %v", err)
}

utils.WriteKustomization(
path,
[]string{"namespace.yaml"},
[]string{
filepath.Join(componentRelPath, "project-admin-rolebindings", projectOwner),
},
)
}

func createRoleBinding(projectName, groupName, roleName string) {
appName := config.GetString("app-name")
bindingName := fmt.Sprintf("project-%s-rolebindings", roleName)
path := filepath.Join(
repoDirectory, appName, componentPath,
bindingName, groupName, "rbac.yaml",
)

if utils.PathExists(filepath.Dir(path)) {
log.Printf("rolebinding already exists (continuing)")
return
}

rbac := models.NewRoleBinding(
fmt.Sprintf("namespace-%s-%s", roleName, groupName),
roleName,
)
rbac.AddGroup(groupName)
rbacOut := models.ToYAML(rbac)

log.Printf("writing rbac definition to %s", filepath.Dir(path))
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
log.Fatalf("failed to create rolebinding directory: %v", err)
}

err := ioutil.WriteFile(path, rbacOut, 0644)
if err != nil {
log.Fatalf("failed to write rbac: %v", err)
}

utils.WriteComponent(
path,
[]string{"rbac.yaml"},
)
}

func createAdminRoleBinding(projectName, projectOwner string) {
createRoleBinding(projectName, projectOwner, "admin")
}

func createGroup(projectOwner string) {
appName := config.GetString("app-name")
path := filepath.Join(repoDirectory, appName, groupPath, projectOwner, "group.yaml")

if utils.PathExists(filepath.Dir(path)) {
log.Printf("group already exists (continuing)")
return
}

group := models.NewGroup(projectOwner)
groupOut := models.ToYAML(group)

log.Printf("writing group definition to %s", filepath.Dir(path))
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
log.Fatalf("failed to create group directory: %v", err)
}

err := ioutil.WriteFile(path, groupOut, 0644)
if err != nil {
log.Fatalf("failed to write group: %v", err)
}

utils.WriteKustomization(
path,
[]string{"group.yaml"},
nil,
)
}

func addGroupRBAC(projectName, groupName, roleName string) {
appName := config.GetString("app-name")
bindingName := fmt.Sprintf("project-%s-rolebindings", roleName)

nsPath := filepath.Join(
repoDirectory, appName, namespacePath, projectName,
)

groupPath := filepath.Join(
repoDirectory, appName, groupPath, groupName,
)

if !utils.PathExists(nsPath) {
log.Fatalf("namespace %s does not exist", projectName)
}

if !utils.PathExists(groupPath) {
log.Fatalf("group %s does not exist", groupName)
}

createRoleBinding(projectName, groupName, roleName)

log.Printf("granting %s role %s on %s", groupName, roleName, projectName)
utils.AddKustomizeComponent(
filepath.Join(componentRelPath, bindingName, groupName),
nsPath,
)
}
21 changes: 21 additions & 0 deletions cmd/createGroup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package cmd

import (
"github.com/spf13/cobra"
)

var createGroupCmd = &cobra.Command{
Use: "create-group group",
Short: "Create a group",
Long: `Create a group.
Create the group resource and associated kustomization file`,
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
createGroup(args[0])
},
}

func init() {
rootCmd.AddCommand(createGroupCmd)
}
24 changes: 24 additions & 0 deletions cmd/createGroup_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package cmd

import (
"path/filepath"

"github.com/stretchr/testify/assert"
)

func (ctx *Context) TestCreateGroupCmd() {
assert := assert.New(ctx.T())

rootCmd.SetArgs([]string{"--repodir", ctx.dir, "create-group", "testgroup"})
err := rootCmd.Execute()
assert.Nil(err)

expectedPaths := []string{
"cluster-scope/base/user.openshift.io/groups/testgroup/group.yaml",
"cluster-scope/base/user.openshift.io/groups/testgroup/kustomization.yaml",
}

for _, path := range expectedPaths {
assert.FileExists(filepath.Join(ctx.dir, path))
}
}
Loading

0 comments on commit 1c6bd0e

Please sign in to comment.