mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-12 19:32:48 -06:00
6601188c64
* runtime: Correct naming of version checking code Rename telemetry functionality to version checking to accurately reflect current behavior following https://github.com/open-policy-agent/opa/pull/7756. The system only checks GitHub releases for version updates without sending any data about the OPA instance and so the privacy docs have been updated too. Signed-off-by: Charlie Egan <charlie_egan@apple.com> * Make WithTelemetryGatherers a no-op Deprecate WithTelemetryGatherers since telemetry gathering has been removed. The function now returns a no-op to maintain API compatibility without breaking existing code that might uses it. Signed-off-by: Charlie Egan <charlie_egan@apple.com> --------- Signed-off-by: Charlie Egan <charlie_egan@apple.com>
211 lines
6.4 KiB
Go
211 lines
6.4 KiB
Go
// Copyright 2020 The OPA Authors. All rights reserved.
|
|
// Use of this source code is governed by an Apache2
|
|
// license that can be found in the LICENSE file.
|
|
|
|
// Package versioncheck provides functions to check for the latest OPA release version from GitHub.
|
|
package versioncheck
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"runtime"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/open-policy-agent/opa/internal/semver"
|
|
"github.com/open-policy-agent/opa/v1/keys"
|
|
"github.com/open-policy-agent/opa/v1/logging"
|
|
"github.com/open-policy-agent/opa/v1/version"
|
|
|
|
"github.com/open-policy-agent/opa/v1/plugins/rest"
|
|
"github.com/open-policy-agent/opa/v1/util"
|
|
)
|
|
|
|
// ExternalServiceURL is the base HTTP URL for a github instance used
|
|
// to query for more recent version.
|
|
// If not otherwise specified, it will use the hard-coded default, api.github.com.
|
|
// GHRepo is the repository to use, and defaults to "open-policy-agent/opa"
|
|
//
|
|
// Override at build time via:
|
|
//
|
|
// -ldflags "-X github.com/open-policy-agent/opa/internal/versioncheck.ExternalServiceURL=<url>"
|
|
// -ldflags "-X github.com/open-policy-agent/opa/internal/versioncheck.GHRepo=<url>"
|
|
//
|
|
// ExternalServiceURL will be overridden if the OPA_VERSION_CHECK_SERVICE_URL environment variable
|
|
// is provided.
|
|
var ExternalServiceURL = "https://api.github.com"
|
|
var GHRepo = "open-policy-agent/opa"
|
|
|
|
// Checker checks for the latest OPA release version
|
|
type Checker interface {
|
|
LatestVersion(ctx context.Context) (*DataResponse, error)
|
|
RegisterGatherer(key string, f Gatherer)
|
|
}
|
|
|
|
// Gatherer represents a mechanism to inject additional data (currently unused for version checking)
|
|
type Gatherer func(ctx context.Context) (any, error)
|
|
|
|
// DataResponse represents the data returned by the version check
|
|
type DataResponse struct {
|
|
Latest ReleaseDetails `json:"latest"`
|
|
}
|
|
|
|
// ReleaseDetails holds information about the latest OPA release
|
|
type ReleaseDetails struct {
|
|
Download string `json:"download,omitempty"` // link to download the OPA release
|
|
ReleaseNotes string `json:"release_notes,omitempty"` // link to the OPA release notes
|
|
LatestRelease string `json:"latest_release,omitempty"` // latest OPA released version
|
|
OPAUpToDate bool `json:"opa_up_to_date,omitempty"` // is running OPA version greater than or equal to the latest released
|
|
}
|
|
|
|
// Options supplies parameters to the version checker.
|
|
type Options struct {
|
|
Logger logging.Logger
|
|
}
|
|
|
|
type GitHubVersionChecker struct {
|
|
client rest.Client
|
|
}
|
|
|
|
type GitHubRelease struct {
|
|
TagName string `json:"tag_name,omitempty"` // latest OPA release tag
|
|
ReleaseNotes string `json:"html_url,omitempty"` // link to the OPA release notes
|
|
Download string `json:"assets_url,omitempty"` // link to download the OPA release
|
|
}
|
|
|
|
// New returns an instance of the Checker
|
|
func New(opts Options) (Checker, error) {
|
|
url := os.Getenv("OPA_VERSION_CHECK_SERVICE_URL")
|
|
if url == "" {
|
|
url = ExternalServiceURL
|
|
}
|
|
|
|
// Set a generic User-Agent to avoid sending version/platform information about the user's OPA instance.
|
|
// This ensures we only retrieve version information without transmitting any identifying data.
|
|
restConfig := fmt.Appendf(nil, `{
|
|
"url": %q,
|
|
"headers": {
|
|
"User-Agent": "OPA-Version-Checker"
|
|
}
|
|
}`, url)
|
|
|
|
client, err := rest.New(restConfig, map[string]*keys.Config{}, rest.Logger(opts.Logger))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
r := GitHubVersionChecker{client: client}
|
|
|
|
return &r, nil
|
|
}
|
|
|
|
// LatestVersion queries the GitHub API to check for the latest OPA release version
|
|
func (r *GitHubVersionChecker) LatestVersion(ctx context.Context) (*DataResponse, error) {
|
|
rCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
|
defer cancel()
|
|
|
|
resp, err := r.client.Do(rCtx, "GET", fmt.Sprintf("/repos/%s/releases/latest", GHRepo))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
defer util.Close(resp)
|
|
|
|
switch resp.StatusCode {
|
|
case http.StatusOK:
|
|
if resp.Body != nil {
|
|
var result GitHubRelease
|
|
err := json.NewDecoder(resp.Body).Decode(&result)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return createReleaseInfo(result)
|
|
}
|
|
return nil, nil
|
|
default:
|
|
return nil, fmt.Errorf("server replied with HTTP %v", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
func createReleaseInfo(ghResp GitHubRelease) (*DataResponse, error) {
|
|
if ghResp.TagName == "" {
|
|
return nil, errors.New("server response does not contain tag_name")
|
|
}
|
|
|
|
sv, err := semver.Parse(version.Version)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse current version %q: %w", version.Version, err)
|
|
}
|
|
|
|
latestSV, err := semver.Parse(ghResp.TagName)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse latest version %q: %w", ghResp.TagName, err)
|
|
}
|
|
|
|
isLatest := sv.Compare(latestSV) >= 0
|
|
|
|
// Note: alternatively, we could look through the assets in the GH API response to find a matching asset,
|
|
// and use its URL. However, this is not guaranteed to be more robust, and wouldn't use the 'openpolicyagent.org' domain.
|
|
downloadLink := fmt.Sprintf("https://openpolicyagent.org/downloads/%v/opa_%v_%v",
|
|
ghResp.TagName, runtime.GOOS, runtime.GOARCH)
|
|
|
|
if runtime.GOARCH == "arm64" {
|
|
downloadLink = fmt.Sprintf("%v_static", downloadLink)
|
|
}
|
|
|
|
if strings.HasPrefix(runtime.GOOS, "win") {
|
|
downloadLink = fmt.Sprintf("%v.exe", downloadLink)
|
|
}
|
|
|
|
return &DataResponse{
|
|
Latest: ReleaseDetails{
|
|
Download: downloadLink,
|
|
ReleaseNotes: ghResp.ReleaseNotes,
|
|
LatestRelease: ghResp.TagName,
|
|
OPAUpToDate: isLatest,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func (*GitHubVersionChecker) RegisterGatherer(_ string, _ Gatherer) {
|
|
// no-op for this implementation
|
|
}
|
|
|
|
// IsSet returns true if dr is populated.
|
|
func (dr *DataResponse) IsSet() bool {
|
|
return dr != nil && dr.Latest.LatestRelease != "" && dr.Latest.Download != "" && dr.Latest.ReleaseNotes != ""
|
|
}
|
|
|
|
// Slice returns the dr as a slice of key-value string pairs. If dr is nil, this function returns an empty slice.
|
|
func (dr *DataResponse) Slice() [][2]string {
|
|
|
|
if !dr.IsSet() {
|
|
return nil
|
|
}
|
|
|
|
return [][2]string{
|
|
{"Latest Upstream Version", strings.TrimPrefix(dr.Latest.LatestRelease, "v")},
|
|
{"Download", dr.Latest.Download},
|
|
{"Release Notes", dr.Latest.ReleaseNotes},
|
|
}
|
|
}
|
|
|
|
// Pretty returns OPA release information in a human-readable format.
|
|
func (dr *DataResponse) Pretty() string {
|
|
if !dr.IsSet() {
|
|
return ""
|
|
}
|
|
|
|
pairs := dr.Slice()
|
|
lines := make([]string, 0, len(pairs))
|
|
|
|
for _, pair := range pairs {
|
|
lines = append(lines, fmt.Sprintf("%v: %v", pair[0], pair[1]))
|
|
}
|
|
|
|
return strings.Join(lines, "\n")
|
|
}
|