Files
woodpecker/cli/internal/config/context.go
2026-01-11 15:32:00 +01:00

246 lines
5.8 KiB
Go

// Copyright 2026 Woodpecker Authors
//
// 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 config
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"github.com/adrg/xdg"
"github.com/rs/zerolog/log"
"github.com/urfave/cli/v3"
"github.com/zalando/go-keyring"
)
// Context represents a single CLI context with its connection details.
type Context struct {
Name string `json:"name"`
ServerURL string `json:"server_url"`
LogLevel string `json:"log_level,omitempty"`
}
// Contexts holds all contexts and tracks the current active one.
type Contexts struct {
CurrentContext string `json:"current_context"`
Contexts map[string]Context `json:"contexts"`
}
func getContextsPath() (string, error) {
configPath, err := xdg.ConfigFile("woodpecker/contexts.json")
if err != nil {
return "", err
}
return configPath, nil
}
// LoadContexts loads all contexts from the contexts file.
func LoadContexts() (*Contexts, error) {
contextsPath, err := getContextsPath()
if err != nil {
return nil, err
}
content, err := os.ReadFile(contextsPath)
if err != nil {
if os.IsNotExist(err) {
return &Contexts{
Contexts: make(map[string]Context),
}, nil
}
return nil, err
}
var contexts Contexts
err = json.Unmarshal(content, &contexts)
if err != nil {
return nil, err
}
if contexts.Contexts == nil {
contexts.Contexts = make(map[string]Context)
}
return &contexts, nil
}
// SaveContexts saves all contexts to the contexts file.
func SaveContexts(contexts *Contexts) error {
data, err := json.MarshalIndent(contexts, "", " ")
if err != nil {
return err
}
contextsPath, err := getContextsPath()
if err != nil {
return err
}
// Ensure the directory exists.
dir := filepath.Dir(contextsPath)
if err := os.MkdirAll(dir, 0o755); err != nil {
return err
}
return os.WriteFile(contextsPath, data, 0o600)
}
// GetCurrentContext returns the current active context.
func GetCurrentContext(ctx context.Context, c *cli.Command) (*Config, error) {
contexts, err := LoadContexts()
if err != nil {
return nil, err
}
if contexts.CurrentContext == "" {
return nil, errors.New("no context is currently set")
}
context, exists := contexts.Contexts[contexts.CurrentContext]
if !exists {
return nil, fmt.Errorf("current context '%s' not found", contexts.CurrentContext)
}
return GetContextConfig(c, &context)
}
// GetContextConfig loads the config for a specific context including the token from keyring.
func GetContextConfig(c *cli.Command, ctx *Context) (*Config, error) {
conf := &Config{
ServerURL: ctx.ServerURL,
LogLevel: ctx.LogLevel,
}
// Load token from keyring
service := c.Root().Name
secret, err := keyring.Get(service, ctx.ServerURL)
if errors.Is(err, keyring.ErrUnsupportedPlatform) {
log.Warn().Msg("keyring is not supported on this platform")
return conf, nil
}
if errors.Is(err, keyring.ErrNotFound) {
return nil, fmt.Errorf("token not found in keyring for context '%s'", ctx.Name)
}
if err != nil {
return nil, err
}
conf.Token = secret
return conf, nil
}
// AddOrUpdateContext adds or updates a context and optionally sets it as current.
func AddOrUpdateContext(c *cli.Command, name, serverURL, token, logLevel string, setCurrent bool) error {
contexts, err := LoadContexts()
if err != nil {
return err
}
contexts.Contexts[name] = Context{
Name: name,
ServerURL: serverURL,
LogLevel: logLevel,
}
if setCurrent || contexts.CurrentContext == "" {
contexts.CurrentContext = name
}
// Save token to keyring
service := c.Root().Name
err = keyring.Set(service, serverURL, token)
if err != nil {
return err
}
return SaveContexts(contexts)
}
// DeleteContext removes a context.
func DeleteContext(c *cli.Command, name string) error {
contexts, err := LoadContexts()
if err != nil {
return err
}
context, exists := contexts.Contexts[name]
if !exists {
return fmt.Errorf("context '%s' not found", name)
}
// Try to delete token from keyring
service := c.Root().Name
err = keyring.Delete(service, context.ServerURL)
if err != nil && !errors.Is(err, keyring.ErrNotFound) {
log.Warn().Err(err).Msg("failed to delete token from keyring")
}
delete(contexts.Contexts, name)
// If we deleted the current context, unset it
if contexts.CurrentContext == name {
contexts.CurrentContext = ""
}
return SaveContexts(contexts)
}
// SetCurrentContext sets the current active context.
func SetCurrentContext(name string) error {
contexts, err := LoadContexts()
if err != nil {
return err
}
if _, exists := contexts.Contexts[name]; !exists {
return fmt.Errorf("context '%s' not found", name)
}
contexts.CurrentContext = name
return SaveContexts(contexts)
}
// RenameContext renames an existing context.
func RenameContext(oldName, newName string) error {
contexts, err := LoadContexts()
if err != nil {
return err
}
context, exists := contexts.Contexts[oldName]
if !exists {
return fmt.Errorf("context '%s' not found", oldName)
}
if _, exists := contexts.Contexts[newName]; exists {
return fmt.Errorf("context '%s' already exists", newName)
}
// Update the name in the context
context.Name = newName
contexts.Contexts[newName] = context
delete(contexts.Contexts, oldName)
// Update current context if necessary
if contexts.CurrentContext == oldName {
contexts.CurrentContext = newName
}
return SaveContexts(contexts)
}