(chore) setup kmsvc-cli
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
kmsvc "forgejo.riotpiao.homelab.com/homelab/kmsvc-sdk"
|
||||
"google.golang.org/grpc/credentials"
|
||||
)
|
||||
|
||||
// defaultTokenURL is the Authentik OAuth2 token endpoint used with
|
||||
// --client-id/--client-secret when --token-url/KMSVC_TOKEN_URL is unset.
|
||||
const defaultTokenURL = "https://authentik.riotpiao.homelab.com/application/o/token/"
|
||||
|
||||
// buildClient constructs a *kmsvc.Client from resolved global flags.
|
||||
//
|
||||
// Defaults to TLS: the SDK itself defaults to plaintext (appropriate for
|
||||
// cluster-internal callers), but kmsvc-cli's own default --server
|
||||
// (kmsvc.riotpiao.homelab.com:443, see README) is reached through an
|
||||
// Ingress-terminated HTTPS/gRPC-passthrough endpoint, so a real external
|
||||
// invocation needs a TLS handshake, not plaintext. --insecure opts back into
|
||||
// plaintext for cluster-internal/dev targets.
|
||||
func buildClient(ctx context.Context, flags *globalFlags) (*kmsvc.Client, error) {
|
||||
if flags.server == "" {
|
||||
return nil, fmt.Errorf("server address required (--server or KMSVC_SERVER)")
|
||||
}
|
||||
|
||||
token := flags.token
|
||||
if token == "" && flags.clientID != "" && flags.clientSecret != "" {
|
||||
tokenURL := flags.tokenURL
|
||||
if tokenURL == "" {
|
||||
tokenURL = defaultTokenURL
|
||||
}
|
||||
fetched, err := fetchClientCredentialsToken(ctx, tokenURL, flags.clientID, flags.clientSecret)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fetch token via client_credentials: %w", err)
|
||||
}
|
||||
token = fetched
|
||||
}
|
||||
|
||||
var opts []kmsvc.Option
|
||||
if token != "" {
|
||||
opts = append(opts, kmsvc.WithTokenSource(kmsvc.StaticToken(token)))
|
||||
}
|
||||
if !flags.insecure {
|
||||
opts = append(opts, kmsvc.WithTransportCredentials(credentials.NewTLS(nil)))
|
||||
}
|
||||
|
||||
client, err := kmsvc.New(ctx, flags.server, opts...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("connect to %s: %w", flags.server, err)
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// fetchClientCredentialsToken performs an OAuth2 client_credentials grant
|
||||
// against tokenURL, used when --token/KMSVC_TOKEN is unset but
|
||||
// --client-id/--client-secret (KMSVC_CLIENT_ID/KMSVC_CLIENT_SECRET) are
|
||||
// configured, so callers don't need a separate curl step to mint a token.
|
||||
func fetchClientCredentialsToken(ctx context.Context, tokenURL, clientID, clientSecret string) (string, error) {
|
||||
if tokenURL == "" {
|
||||
return "", fmt.Errorf("token URL required (--token-url or KMSVC_TOKEN_URL)")
|
||||
}
|
||||
|
||||
form := url.Values{
|
||||
"grant_type": {"client_credentials"},
|
||||
"client_id": {clientID},
|
||||
"client_secret": {clientSecret},
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, tokenURL, strings.NewReader(form.Encode()))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("token endpoint returned %s", resp.Status)
|
||||
}
|
||||
|
||||
var body struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
||||
return "", fmt.Errorf("decode token response: %w", err)
|
||||
}
|
||||
if body.AccessToken == "" {
|
||||
return "", fmt.Errorf("token response missing access_token")
|
||||
}
|
||||
return body.AccessToken, nil
|
||||
}
|
||||
Reference in New Issue
Block a user