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
14 changes: 14 additions & 0 deletions docs/azdo_help_reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -1037,6 +1037,20 @@ Aliases
cr, c, new, n, add, a
```

#### `azdo service-endpoint create github [ORGANIZATION/]PROJECT --name NAME [--url URL] [--token TOKEN] [flags]`

Create a GitHub service endpoint

```
--configuration-id string Configuration for connecting to the endpoint (use an OAuth/installation configuration). Mutually exclusive with --token.
-q, --jq expression Filter JSON output using a jq expression
--json fields[=*] Output JSON with the specified fields. Prefix a field with '-' to exclude it.
--name string Name of the service endpoint
-t, --template string Format JSON output using a Go template; see "azdo help formatting"
--token string Visit https://github.com/settings/tokens to create personal access tokens. Recommended scopes: repo, user, admin:repo_hook. If omitted, you will be prompted for a token when interactive.
--url string GitHub URL (defaults to https://github.com)
```

### `azdo service-endpoint delete [ORGANIZATION/]PROJECT/ID_OR_NAME [flags]`

Delete a service endpoint from a project.
Expand Down
1 change: 1 addition & 0 deletions docs/azdo_service-endpoint_create.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Check the available subcommands to create service connections of specific well-k
### Available commands

* [azdo service-endpoint create azurerm](./azdo_service-endpoint_create_azurerm.md)
* [azdo service-endpoint create github](./azdo_service-endpoint_create_github.md)

### Options

Expand Down
58 changes: 58 additions & 0 deletions docs/azdo_service-endpoint_create_github.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
## Command `azdo service-endpoint create github`

```
azdo service-endpoint create github [ORGANIZATION/]PROJECT --name NAME [--url URL] [--token TOKEN] [flags]
```

Create a GitHub service endpoint using a personal access token (PAT) or an installation/oauth configuration.


### Options


* `--configuration-id` `string`

Configuration for connecting to the endpoint (use an OAuth/installation configuration). Mutually exclusive with --token.

* `-q`, `--jq` `expression`

Filter JSON output using a jq expression

* `--json` `fields`

Output JSON with the specified fields. Prefix a field with '-' to exclude it.

* `--name` `string`

Name of the service endpoint

* `-t`, `--template` `string`

Format JSON output using a Go template; see "azdo help formatting"

* `--token` `string`

Visit https://github.com/settings/tokens to create personal access tokens. Recommended scopes: repo, user, admin:repo_hook. If omitted, you will be prompted for a token when interactive.

* `--url` `string`

GitHub URL (defaults to https://github.com)


### JSON Fields

`administratorsGroup`, `authorization`, `createdBy`, `data`, `description`, `groupScopeId`, `id`, `isReady`, `isShared`, `name`, `operationStatus`, `owner`, `readersGroup`, `serviceEndpointProjectReferences`, `type`, `url`

### Examples

```bash
# Create a GitHub service endpoint with a personal access token (PAT)
azdo service-endpoint create github my-org/my-project --name "gh-ep" --token <PAT>

# Create a GitHub service endpoint with an installation / OAuth configuration id
azdo service-endpoint create github my-org/my-project --name "gh-ep" --configuration-id <CONFIG_ID>
```

### See also

* [azdo service-endpoint create](./azdo_service-endpoint_create.md)
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ import (
"github.com/tmeckel/azdo-cli/internal/types"
)

type contextKey string

const (
ctxKeyCreateOpts contextKey = "azurerm/create-opts"
ctxKeyCertPath contextKey = "azurerm/cert-path"
)

const (
testCertificatePEM = `-----BEGIN CERTIFICATE-----
MIIDXTCCAkWgAwIBAgIJAKoK/heBjcOuMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV
Expand Down
8 changes: 0 additions & 8 deletions internal/cmd/serviceendpoint/create/azurerm/test_keys.go

This file was deleted.

3 changes: 3 additions & 0 deletions internal/cmd/serviceendpoint/create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"golang.org/x/text/encoding/unicode"

"github.com/tmeckel/azdo-cli/internal/cmd/serviceendpoint/create/azurerm"
"github.com/tmeckel/azdo-cli/internal/cmd/serviceendpoint/create/github"
"github.com/tmeckel/azdo-cli/internal/cmd/serviceendpoint/shared"
"github.com/tmeckel/azdo-cli/internal/cmd/util"
"github.com/tmeckel/azdo-cli/internal/types"
Expand Down Expand Up @@ -98,6 +99,8 @@ func NewCmd(ctx util.CmdContext) *cobra.Command {

cmd.AddCommand(azurerm.NewCmd(ctx))

cmd.AddCommand(github.NewCmd(ctx))

return cmd
}

Expand Down
208 changes: 208 additions & 0 deletions internal/cmd/serviceendpoint/create/github/create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
package github

import (
"fmt"

"github.com/MakeNowJust/heredoc"
"github.com/google/uuid"
"github.com/microsoft/azure-devops-go-api/azuredevops/v7/serviceendpoint"
"github.com/spf13/cobra"
"go.uber.org/zap"

"github.com/tmeckel/azdo-cli/internal/cmd/serviceendpoint/shared"
"github.com/tmeckel/azdo-cli/internal/cmd/util"
"github.com/tmeckel/azdo-cli/internal/types"
)

type createOptions struct {
project string

name string
url string
token string
configurationID string

exporter util.Exporter
}

func NewCmd(ctx util.CmdContext) *cobra.Command {
opts := &createOptions{}

cmd := &cobra.Command{
Use: "github [ORGANIZATION/]PROJECT --name NAME [--url URL] [--token TOKEN]",
Short: "Create a GitHub service endpoint",
Long: heredoc.Doc(`
Create a GitHub service endpoint using a personal access token (PAT) or an installation/oauth configuration.
`),
Example: heredoc.Doc(`
# Create a GitHub service endpoint with a personal access token (PAT)
azdo service-endpoint create github my-org/my-project --name "gh-ep" --token <PAT>

# Create a GitHub service endpoint with an installation / OAuth configuration id
azdo service-endpoint create github my-org/my-project --name "gh-ep" --configuration-id <CONFIG_ID>
`),
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
opts.project = args[0]
return runCreate(ctx, opts)
},
}

cmd.Flags().StringVar(&opts.name, "name", "", "Name of the service endpoint")
cmd.Flags().StringVar(&opts.url, "url", "", "GitHub URL (defaults to https://github.com)")
// Help text taken from service-endpoint-types.json (inputDescriptors.AccessToken.description)
cmd.Flags().StringVar(&opts.token, "token", "", "Visit https://github.com/settings/tokens to create personal access tokens. Recommended scopes: repo, user, admin:repo_hook. If omitted, you will be prompted for a token when interactive.")
// Support installation/oauth configuration via ConfigurationId (InstallationToken scheme)
// Help text taken from service-endpoint-types.json (inputDescriptors.ConfigurationId.description)
cmd.Flags().StringVar(&opts.configurationID, "configuration-id", "", "Configuration for connecting to the endpoint (use an OAuth/installation configuration). Mutually exclusive with --token.")

_ = cmd.MarkFlagRequired("name")

util.AddJSONFlags(cmd, &opts.exporter, []string{
"administratorsGroup",
"authorization",
"createdBy",
"data",
"description",
"groupScopeId",
"id",
"isReady",
"isShared",
"name",
"operationStatus",
"owner",
"readersGroup",
"serviceEndpointProjectReferences",
"type",
"url",
})

return cmd
}

func runCreate(ctx util.CmdContext, opts *createOptions) error {
ios, err := ctx.IOStreams()
if err != nil {
return err
}

p, err := ctx.Prompter()
if err != nil {
return err
}

scope, err := util.ParseProjectScope(ctx, opts.project)
if err != nil {
return util.FlagErrorWrap(err)
}

// default URL
if opts.url == "" {
opts.url = "https://github.com"
}

// authentication selection: token (PAT) or configuration-id (InstallationToken)
if opts.token != "" && opts.configurationID != "" {
return fmt.Errorf("--token and --configuration-id are mutually exclusive")
}
if opts.token == "" && opts.configurationID == "" {
// default to prompting for token when interactive
if !ios.CanPrompt() {
return fmt.Errorf("no authentication provided: pass --token or --configuration-id (and enable prompting to provide token interactively)")
}
secret, err := p.Password("GitHub token:")
if err != nil {
return fmt.Errorf("prompt for token failed: %w", err)
}
opts.token = secret
}

projectRef, err := shared.ResolveProjectReference(ctx, scope)
if err != nil {
return util.FlagErrorWrap(err)
}

endpointType := "github"
owner := "library"

var scheme string
var authParams map[string]string
if opts.configurationID != "" {
// InstallationToken scheme expects ConfigurationId parameter
scheme = "InstallationToken"
authParams = map[string]string{
"ConfigurationId": opts.configurationID,
}
} else {
// default to PAT token
scheme = "Token"
authParams = map[string]string{
"AccessToken": opts.token,
}
}

endpoint := &serviceendpoint.ServiceEndpoint{
Name: &opts.name,
Type: &endpointType,
Url: &opts.url,
Owner: &owner,
Authorization: &serviceendpoint.EndpointAuthorization{
Scheme: &scheme,
Parameters: &authParams,
},
ServiceEndpointProjectReferences: &[]serviceendpoint.ServiceEndpointProjectReference{
{
ProjectReference: projectRef,
Name: &opts.name,
Description: types.ToPtr(""),
},
},
}

ios.StartProgressIndicator()
defer ios.StopProgressIndicator()

client, err := ctx.ClientFactory().ServiceEndpoint(ctx.Context(), scope.Organization)
if err != nil {
return fmt.Errorf("failed to create service endpoint client: %w", err)
}

createdEndpoint, err := client.CreateServiceEndpoint(ctx.Context(), serviceendpoint.CreateServiceEndpointArgs{
Endpoint: endpoint,
})
if err != nil {
return fmt.Errorf("failed to create service endpoint: %w", err)
}

zap.L().Debug("github service endpoint created",
zap.String("id", types.GetValue(createdEndpoint.Id, uuid.Nil).String()),
zap.String("name", types.GetValue(createdEndpoint.Name, "")),
)

ios.StopProgressIndicator()

if opts.exporter != nil {
// redact authorization.parameters before emitting JSON to avoid leaking secrets
if createdEndpoint.Authorization != nil && createdEndpoint.Authorization.Parameters != nil {
for k := range *createdEndpoint.Authorization.Parameters {
(*createdEndpoint.Authorization.Parameters)[k] = "REDACTED"
}
}
return opts.exporter.Write(ios, createdEndpoint)
}

tp, err := ctx.Printer("list")
if err != nil {
return err
}
tp.AddColumns("ID", "Name", "Type", "URL")
tp.EndRow()
tp.AddField(types.GetValue(createdEndpoint.Id, uuid.Nil).String())
tp.AddField(types.GetValue(createdEndpoint.Name, ""))
tp.AddField(types.GetValue(createdEndpoint.Type, ""))
tp.AddField(types.GetValue(createdEndpoint.Url, ""))
tp.EndRow()
tp.Render()

return nil
}
Loading
Loading