Skip to content

Commit a50db9f

Browse files
authored
Merge branch 'main' into renovate/golang.org-x-mod-0.x
Signed-off-by: Drew Raines <draines@datum.net>
2 parents 1d4d11c + 1eae24e commit a50db9f

8 files changed

Lines changed: 77 additions & 12 deletions

File tree

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ require (
2020
github.com/spf13/pflag v1.0.10
2121
github.com/zalando/go-keyring v0.2.8
2222
go.miloapis.com/activity v0.7.0
23-
go.miloapis.com/milo v0.27.0
24-
golang.org/x/mod v0.37.0
23+
go.miloapis.com/milo v0.28.1
24+
golang.org/x/mod v0.35.0
2525
golang.org/x/oauth2 v0.36.0
2626
golang.org/x/term v0.43.0
2727
k8s.io/api v0.35.3

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,8 @@ go.miloapis.com/activity v0.7.0 h1:Nmc5XzA4oEMTko5/ciJAeERVk18FaSnRpTBo0Sm89YU=
249249
go.miloapis.com/activity v0.7.0/go.mod h1:Sh2Irbq6siJcfq17nLjHvm4JHN/2Csc5YCHB+ycz20c=
250250
go.miloapis.com/milo v0.27.0 h1:lbUZJhWBgFmvdINyt6oonWEAW2bwuoW9CVa9D1mCYSI=
251251
go.miloapis.com/milo v0.27.0/go.mod h1:p9O2kk194mvoL8rhqjwb+LWB+GIyY4vQqiTowwibVWo=
252+
go.miloapis.com/milo v0.28.1 h1:30bQS4EwadbOBsn3UC8qSBENN1koQgsQpcvqPjXfmlI=
253+
go.miloapis.com/milo v0.28.1/go.mod h1:p9O2kk194mvoL8rhqjwb+LWB+GIyY4vQqiTowwibVWo=
252254
go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c=
253255
go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE=
254256
go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0=

internal/cmd/plugin/helpers.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"crypto/sha256"
1010
"encoding/hex"
1111
"encoding/json"
12+
"errors"
1213
"fmt"
1314
"io"
1415
"net/http"
@@ -23,9 +24,24 @@ import (
2324

2425
"golang.org/x/mod/semver"
2526

27+
customerrors "go.datum.net/datumctl/internal/errors"
2628
"go.datum.net/datumctl/internal/pluginstore"
2729
)
2830

31+
// indexFetchUserError converts a RefreshIndex failure into a user-facing error,
32+
// attaching actionable guidance when the cause is recognizable (e.g. a GitHub
33+
// token in the environment being rejected by the index host).
34+
func indexFetchUserError(err error) error {
35+
msg := "could not fetch the plugin index: " + err.Error()
36+
var fe *pluginstore.IndexFetchError
37+
if errors.As(err, &fe) {
38+
if hint := fe.Hint(); hint != "" {
39+
return customerrors.NewUserErrorWithHint(msg, hint)
40+
}
41+
}
42+
return customerrors.NewUserError(msg)
43+
}
44+
2945
const (
3046
pluginDownloadTimeout = 60 * time.Second
3147
manifestReadTimeout = 5 * time.Second

internal/cmd/plugin/install.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ The plugin binary is written to the managed plugins directory
7676
// Curated index path.
7777
idx, idxErr := loadOrRefreshIndex(cmd)
7878
if idxErr != nil {
79-
return customerrors.NewUserError("could not fetch plugin index: " + idxErr.Error())
79+
return indexFetchUserError(idxErr)
8080
}
8181
entry, pluginName, binaryPath, installErr := installPlugin(cmd.Context(), pluginsDir, arg, "", currentVersion, idx)
8282
if installErr != nil {

internal/cmd/plugin/search.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77

88
"github.com/spf13/cobra"
99

10-
customerrors "go.datum.net/datumctl/internal/errors"
1110
"go.datum.net/datumctl/internal/pluginstore"
1211
)
1312

@@ -33,7 +32,7 @@ Run 'datumctl plugin install <name>' to install a plugin listed here.`,
3332
idx, err = pluginstore.RefreshIndex(cmd.Context())
3433
if err != nil {
3534
if idx == nil {
36-
return customerrors.NewUserError("could not fetch plugin index: " + err.Error())
35+
return indexFetchUserError(err)
3736
}
3837
fmt.Fprintf(cmd.ErrOrStderr(), "warning: index refresh failed (%v), showing cached results\n", err)
3938
}

internal/cmd/plugin/upgrade.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ compatibility validation.`,
5454
fmt.Fprintf(cmd.ErrOrStderr(), "warning: index refresh failed (%v), using cached index\n", refreshErr)
5555
default:
5656
// No cache at all.
57-
return customerrors.NewUserError(fmt.Sprintf("could not fetch plugin index: %v", refreshErr))
57+
return indexFetchUserError(refreshErr)
5858
}
5959

6060
var newEntry *pluginstore.InstalledPlugin

internal/cmd/root.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"encoding/hex"
77
"fmt"
88
"io"
9+
"net/http"
910
"os"
1011
"path/filepath"
1112
"strings"
@@ -14,6 +15,7 @@ import (
1415
"github.com/spf13/cobra"
1516
activity "go.miloapis.com/activity/pkg/cmd"
1617
"k8s.io/cli-runtime/pkg/genericclioptions"
18+
"k8s.io/client-go/transport"
1719
componentversion "k8s.io/component-base/version"
1820
"k8s.io/kubectl/pkg/cmd/apiresources"
1921
"k8s.io/kubectl/pkg/cmd/apply"
@@ -108,6 +110,11 @@ Get started:
108110
// plugin dispatch logic can handle them before Cobra rejects them.
109111
Args: cobra.ArbitraryArgs,
110112
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
113+
// Make the -v flag dump HTTP requests from bare net/http callers,
114+
// as client-go already does for kubectl-backed commands. No-op
115+
// below -v 6.
116+
http.DefaultTransport = transport.DebugWrappers(http.DefaultTransport)
117+
111118
format, _ := cmd.Flags().GetString("error-format")
112119
switch format {
113120
case customerrors.FormatHuman, customerrors.FormatJSON, customerrors.FormatYAML:

internal/pluginstore/index.go

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,8 @@ func RefreshIndex(ctx context.Context) (*CachedIndex, error) {
133133
}
134134

135135
// Attach GitHub token if available.
136-
if token := githubToken(); token != "" {
136+
token, tokenSource := githubTokenWithSource()
137+
if token != "" {
137138
req.Header.Set("Authorization", "Bearer "+token)
138139
}
139140
req.Header.Set("User-Agent", "datumctl-plugin-index")
@@ -145,7 +146,12 @@ func RefreshIndex(ctx context.Context) (*CachedIndex, error) {
145146
defer resp.Body.Close()
146147

147148
if resp.StatusCode != http.StatusOK {
148-
return degradedFallback(fmt.Errorf("fetch plugin index: HTTP %s", resp.Status))
149+
return degradedFallback(&IndexFetchError{
150+
URL: IndexURL,
151+
StatusCode: resp.StatusCode,
152+
Status: resp.Status,
153+
TokenSource: tokenSource,
154+
})
149155
}
150156

151157
raw, err := io.ReadAll(resp.Body)
@@ -202,10 +208,45 @@ func degradedFallback(origErr error) (*CachedIndex, error) {
202208
return cached, origErr
203209
}
204210

205-
// githubToken returns a GitHub personal access token from the environment.
206-
func githubToken() string {
211+
// githubTokenWithSource returns a GitHub personal access token from the
212+
// environment along with the name of the variable it came from (empty when no
213+
// token is set).
214+
func githubTokenWithSource() (token, source string) {
207215
if t := os.Getenv("DATUMCTL_GITHUB_TOKEN"); t != "" {
208-
return t
216+
return t, "DATUMCTL_GITHUB_TOKEN"
209217
}
210-
return os.Getenv("GITHUB_TOKEN")
218+
if t := os.Getenv("GITHUB_TOKEN"); t != "" {
219+
return t, "GITHUB_TOKEN"
220+
}
221+
return "", ""
222+
}
223+
224+
// IndexFetchError is returned by RefreshIndex when the index host responds with
225+
// a non-OK HTTP status. It carries enough context for the command layer to
226+
// render actionable guidance via Hint.
227+
type IndexFetchError struct {
228+
URL string
229+
StatusCode int
230+
Status string // HTTP status text, e.g. "404 Not Found"
231+
TokenSource string // env var the Authorization token came from, "" if none
232+
}
233+
234+
func (e *IndexFetchError) Error() string {
235+
return fmt.Sprintf("the plugin index host returned HTTP %s", e.Status)
236+
}
237+
238+
// Hint returns actionable guidance for resolving the failure, or "" when none
239+
// applies. The common case: a GitHub token in the environment is sent to the
240+
// public index host, which rejects it with a 401/403/404.
241+
func (e *IndexFetchError) Hint() string {
242+
switch e.StatusCode {
243+
case http.StatusUnauthorized, http.StatusForbidden, http.StatusNotFound:
244+
if e.TokenSource != "" {
245+
return fmt.Sprintf(
246+
"A GitHub token from $%s is being sent to the index host, which is the likely cause. "+
247+
"The public plugin index needs no authentication; unset that variable and retry.",
248+
e.TokenSource)
249+
}
250+
}
251+
return ""
211252
}

0 commit comments

Comments
 (0)