Skip to content

Conversation

@Eug1n1
Copy link
Contributor

@Eug1n1 Eug1n1 commented Dec 1, 2025

Summary:

Introduces a new provider servercore to fetch secrets from Servercore Secrets Manager.

Auth

Required environment variables:
SERVERCORE_USERNAME
SERVERCORE_PASSWORD
SERVERCORE_ACCOUNT_ID
SERVERCORE_PROJECT_NAME

URI formats:

Fetch string secret:

ref+servercore://SECRET_NAME

Fetch a leaf value from JSON/YAML secret:

ref+servercore://SECRET_NAME#/path/to/leaf

Examples

CLI:

vals get 'ref+servercore://my-json'
vals get 'ref+servercore://my-json#/baz/mykey'

Notes

  • API returns a base64 string in version.value; provider decodes it, then parses JSON (YAML fallback).

Docs

Servercore Secrets API: https://docs.servercore.com/api/secrets-manager-secrets/

Signed-off-by: Shumskiy Evgeniy <vkzheny@gmail.com>
Signed-off-by: Shumskiy Evgeniy <vkzheny@gmail.com>
Signed-off-by: Shumskiy Evgeniy <vkzheny@gmail.com>
Signed-off-by: Shumskiy Evgeniy <vkzheny@gmail.com>
Signed-off-by: Shumskiy Evgeniy <vkzheny@gmail.com>
@Eug1n1 Eug1n1 force-pushed the feat/servercore-provider branch from ea529c9 to 644354d Compare December 1, 2025 19:49
Signed-off-by: Shumskiy Evgeniy <vkzheny@gmail.com>
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a new Servercore provider to vals, enabling users to retrieve secrets from Servercore Secrets Manager. The implementation follows the established pattern of other vals providers with authentication via environment variables and support for both string and structured (JSON/YAML) secret retrieval.

Key changes:

  • Adds a new servercore provider with HTTP-based authentication and secret retrieval
  • Integrates the provider into the vals runtime with the ref+servercore:// URI scheme
  • Documents the provider usage, authentication requirements, and URI formats in the README

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 7 comments.

File Description
vals.go Registers the new Servercore provider constant and integrates it into the provider factory
pkg/providers/servercore/servercore.go Implements the core provider with HTTP client, token-based auth with retry logic, and GetString/GetStringMap methods
pkg/providers/servercore/servercore_helpers.go Defines auth payload structures and environment variable helpers for credentials
README.md Documents the Servercore provider with authentication requirements, URI formats, examples, and API reference

Eug1n1 and others added 4 commits December 8, 2025 11:22
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Shumskiy Evgeniy <vkzheny@gmail.com>
Signed-off-by: Shumskiy Evgeniy <vkzheny@gmail.com>
Signed-off-by: Shumskiy Evgeniy <vkzheny@gmail.com>
Signed-off-by: Shumskiy Evgeniy <vkzheny@gmail.com>
@Eug1n1 Eug1n1 force-pushed the feat/servercore-provider branch from 76d8cac to 2806b01 Compare December 8, 2025 08:32
Signed-off-by: Shumskiy Evgeniy <vkzheny@gmail.com>
@Eug1n1
Copy link
Contributor Author

Eug1n1 commented Dec 18, 2025

pls, let me know if i did something wrong

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.

Eug1n1 and others added 2 commits December 20, 2025 17:56
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Shumskiy Evgeniy <vkzheny@gmail.com>
…th section

Signed-off-by: Shumskiy Evgeniy <vkzheny@gmail.com>
@Eug1n1 Eug1n1 force-pushed the feat/servercore-provider branch from 07fe9b6 to e1ac8dd Compare December 20, 2025 15:00
Signed-off-by: Shumskiy Evgeniy <vkzheny@gmail.com>
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

ref+httpjson://api.github.com/users/helmfile/repos?floatAsInt=true#///*[1]/id
```

### Servercore secret manager
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The section header should use title case for "Secret Manager" to match the capitalization pattern used in other provider section headers (e.g., "Scaleway Secret Manager" on line 1104). Change "secret manager" to "Secret Manager" for consistency.

Suggested change
### Servercore secret manager
### Servercore Secret Manager

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +208
package servercore

import (
"bytes"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"time"

"gopkg.in/yaml.v3"

"github.com/helmfile/vals/pkg/api"
"github.com/helmfile/vals/pkg/log"
)

const (
AuthURL = "https://cloud.api.servercore.com/identity/v3/auth/tokens"
SecretBaseURL = "https://cloud.api.servercore.com/secrets-manager/v1/"
usernameEnv = "SERVERCORE_USERNAME"
passwordEnv = "SERVERCORE_PASSWORD"
accountIDEnv = "SERVERCORE_ACCOUNT_ID"
projectNameEnv = "SERVERCORE_PROJECT_NAME"
)

var (
ErrNotFound = errors.New("secret not found")
ErrUnauthorized = errors.New("unauthorized")
ErrForbidden = errors.New("forbidden")
)

type provider struct {
logger *log.Logger
client *http.Client
tokenErr error
token string
}

func New(l *log.Logger, cfg api.StaticConfig) *provider {
// cfg is accepted to satisfy the provider interface; this provider relies solely on
// environment variables (e.g., SERVERCORE_* env vars) for configuration.
client := &http.Client{Timeout: 10 * time.Second}

p := &provider{
logger: l,
client: client,
}

// Acquire token during initialization (token is valid for 24 hours)
p.logger.Debugf("servercore: acquiring token during initialization")
token, err := p.acquireToken()
if err != nil {
p.tokenErr = err
p.logger.Debugf("servercore: failed to acquire token: %v", err)
} else {
p.token = token
p.logger.Debugf("servercore: provider initialized with token")
}

return p
}

func (p *provider) getToken() (string, error) {
if p.tokenErr != nil {
return "", p.tokenErr
}
return p.token, nil
}

func (p *provider) acquireToken() (string, error) {
envs, err := newAuthEnv()
if err != nil {
return "", err
}

payload := newAuthPayload(envs.Username, envs.Password, envs.AccountID, envs.ProjectName)

p.logger.Debugf("servercore: auth request")
hdr, err := p.sendJSON(http.MethodPost, AuthURL, nil, payload, nil, http.StatusCreated)
if err != nil {
return "", fmt.Errorf("servercore: auth request failed: %w", err)
}

token := hdr.Get("X-Subject-Token")
if token == "" {
return "", fmt.Errorf("servercore: missing X-Subject-Token")
}

p.logger.Debugf("servercore: auth success")
return token, nil
}

func (p *provider) sendJSONWithAuth(method string, url string, in any, out any, successStatus int) (http.Header, error) {
token, err := p.getToken()
if err != nil {
return nil, fmt.Errorf("servercore: auth error: %w", err)
}
headers := map[string]string{"X-Auth-Token": token}
p.logger.Debugf("servercore: request with auth: %s %s", method, url)
hdr, err := p.sendJSON(method, url, headers, in, out, successStatus)
if err != nil {
p.logger.Debugf("servercore: request failed: %s %s: %v", method, url, err)
return nil, err
}

p.logger.Debugf("servercore: request ok: %s %s", method, url)
return hdr, nil
}

func (p *provider) sendJSON(method string, url string, headers map[string]string, in any, out any, successStatus int) (http.Header, error) {
var body io.Reader
if in != nil {
b, err := json.Marshal(in)
if err != nil {
return nil, fmt.Errorf("servercore: marshal: %w", err)
}
body = bytes.NewReader(b)
}

p.logger.Debugf("servercore: sending request: %s %s", method, url)
req, err := http.NewRequest(method, url, body)
if err != nil {
return nil, fmt.Errorf("servercore: request: %w", err)
}
if in != nil {
req.Header.Set("Content-Type", "application/json")
}
req.Header.Set("Accept", "application/json")
for k, v := range headers {
req.Header.Set(k, v)
}

resp, err := p.client.Do(req)
if err != nil {
return nil, fmt.Errorf("servercore: do: %w", err)
}
defer func() { _ = resp.Body.Close() }()

switch resp.StatusCode {
case http.StatusNotFound:
p.logger.Debugf("servercore: response status 404 for %s %s", method, url)
return nil, ErrNotFound
case http.StatusUnauthorized:
p.logger.Debugf("servercore: response status 401 for %s %s", method, url)
return nil, ErrUnauthorized
case http.StatusForbidden:
p.logger.Debugf("servercore: response status 403 for %s %s", method, url)
return nil, ErrForbidden
case successStatus:
p.logger.Debugf("servercore: response status %d for %s %s", resp.StatusCode, method, url)
default:
p.logger.Debugf("servercore: response unexpected status %d for %s %s", resp.StatusCode, method, url)
return nil, fmt.Errorf("servercore: unexpected status %d", resp.StatusCode)
}

if out != nil {
if err := json.NewDecoder(resp.Body).Decode(out); err != nil {
return resp.Header, fmt.Errorf("servercore: json decode: %w", err)
}
} else {
_, _ = io.Copy(io.Discard, resp.Body)
}
return resp.Header, nil
}

func (p *provider) GetString(key string) (string, error) {
p.logger.Debugf("servercore: get string for secret=%s", key)
secretURL, err := url.JoinPath(SecretBaseURL, key)
if err != nil {
return "", fmt.Errorf("servercore: error generating secret url: %w", err)
}

var response secretResp
if _, err := p.sendJSONWithAuth(http.MethodGet, secretURL, nil, &response, http.StatusOK); err != nil {
return "", err
}

decoded, err := base64.StdEncoding.DecodeString(response.Version.Value)
if err != nil {
return "", fmt.Errorf("servercore: b64 decode: %w", err)
}

p.logger.Debugf("servercore: get string ok for secret=%s", key)
return string(decoded), nil
}

func (p *provider) GetStringMap(key string) (map[string]any, error) {
p.logger.Debugf("servercore: get map for secret=%s", key)
value, err := p.GetString(key)
if err != nil {
return nil, fmt.Errorf("servercore: get string: %w", err)
}

m := make(map[string]any)
if jerr := json.Unmarshal([]byte(value), &m); jerr != nil {
p.logger.Debugf("servercore: json decode failed for secret=%s, trying yaml", key)
// Fallback to YAML
if yerr := yaml.Unmarshal([]byte(value), &m); yerr != nil {
return nil, fmt.Errorf("servercore: failed to decode secret as JSON or YAML: json error: %v, yaml error: %w", jerr, yerr)
}
}

p.logger.Debugf("servercore: get map ok for secret=%s", key)
return m, nil
}
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new provider lacks test coverage. The repository follows a pattern where providers have corresponding test files (e.g., vals_scaleway_test.go, vals_httpjson_test.go). Consider adding a vals_servercore_test.go file to test the provider's functionality, including authentication, secret retrieval, and error handling scenarios.

Copilot uses AI. Check for mistakes.
Comment on lines +52 to +71
// Acquire token during initialization (token is valid for 24 hours)
p.logger.Debugf("servercore: acquiring token during initialization")
token, err := p.acquireToken()
if err != nil {
p.tokenErr = err
p.logger.Debugf("servercore: failed to acquire token: %v", err)
} else {
p.token = token
p.logger.Debugf("servercore: provider initialized with token")
}

return p
}

func (p *provider) getToken() (string, error) {
if p.tokenErr != nil {
return "", p.tokenErr
}
return p.token, nil
}
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The authentication token is acquired once during provider initialization and stored for the lifetime of the provider instance. According to the comment on line 52, the token is valid for 24 hours. However, there is no mechanism to refresh the token when it expires. If vals runs for more than 24 hours or if the provider is used after the token expires, all requests will fail with authorization errors. Consider implementing token refresh logic that checks token expiration and reacquires it when necessary, or handle 401 Unauthorized responses by refreshing the token and retrying the request.

Copilot uses AI. Check for mistakes.
yxxhero and others added 2 commits December 24, 2025 08:41
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@yxxhero yxxhero merged commit 16e8792 into helmfile:main Dec 24, 2025
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants