Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 140 additions & 0 deletions api/everest/v1alpha1/monitoringconfig_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@
package v1alpha1

import (
"context"
"fmt"
"net/http"

"github.com/AlekSi/pointer"
goversion "github.com/hashicorp/go-version"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

Expand All @@ -29,8 +36,24 @@
MonitoringConfigCredentialsSecretUsernameKey = "username"
// MonitoringConfigCredentialsSecretAPIKeyKey is the credentials secret's key that contains the API key.
MonitoringConfigCredentialsSecretAPIKeyKey = "apiKey"

// PMM2ClientImage is the image for PMM2 client.
PMM2ClientImage = "percona/pmm-client:2"
// PMM3ClientImage is the image for PMM3 client.
PMM3ClientImage = "percona/pmm-client:3"
)

var pmmServerKeys = map[EngineType]pmmKeyPair{
DatabaseEnginePSMDB: {"PMM_SERVER_API_KEY", "PMM_SERVER_TOKEN"},
DatabaseEnginePostgresql: {"PMM_SERVER_KEY", "PMM_SERVER_TOKEN"},
DatabaseEnginePXC: {"pmmserverkey", "pmmservertoken"},
}

type pmmKeyPair struct {
Legacy string
New string
}

// MonitoringConfigSpec defines the desired state of MonitoringConfig.
type MonitoringConfigSpec struct {
// Type is type of monitoring.
Expand Down Expand Up @@ -64,6 +87,68 @@
InUse bool `json:"inUse,omitempty"`
// LastObservedGeneration is the most recent generation observed for this MonitoringConfig.
LastObservedGeneration int64 `json:"lastObservedGeneration,omitempty"`
// PMMServerVersion shows PMM server version
PMMServerVersion PMMServerVersion `json:"pmmServerVersion,omitempty"`
}

// CreatePMMApiKey creates a new API key in PMM by using the provided username and password.
func (c *PMMConfig) CreatePMMApiKey(
ctx context.Context,
apiKeyName, user, password string,
skipTLSVerify bool,
) (string, error) {
auth := basicAuth{
user: user,
password: password,
}
version, err := c.getPMMVersion(ctx, auth, skipTLSVerify)
if err != nil {
return "", err
}

// PMM2 and PMM3 use different API to create tokens
if version.UsesLegacyAuth() {
return createKey(ctx, c.URL, apiKeyName, auth, skipTLSVerify)
} else {

Check failure on line 112 in api/everest/v1alpha1/monitoringconfig_types.go

View workflow job for this annotation

GitHub Actions / golangci-lint

indent-error-flow: if block ends with a return statement, so drop this else and outdent its block (revive)
return createServiceAccountAndToken(ctx, c.URL, apiKeyName, auth, skipTLSVerify)
}
}

func (m *MonitoringConfig) GetPMMServerVersion(ctx context.Context, credentialsSecret *corev1.Secret) (PMMServerVersion, error) {

Check failure on line 117 in api/everest/v1alpha1/monitoringconfig_types.go

View workflow job for this annotation

GitHub Actions / golangci-lint

exported: exported method MonitoringConfig.GetPMMServerVersion should have comment or be unexported (revive)
if key := m.Spec.PMM.getPMMKey(credentialsSecret); key != "" {
skipVerifyTLS := !pointer.Get(m.Spec.VerifyTLS)
return m.Spec.PMM.getPMMVersion(ctx, bearerAuth{token: key}, skipVerifyTLS)
}
return "", nil
}

// getPMMKey finds the PMM key in the given secret
func (c *PMMConfig) getPMMKey(secret *corev1.Secret) string {
if secret == nil || len(secret.Data) == 0 {
return ""
}
// check for all engines, first the legacy keys, then the new case
for _, pair := range pmmServerKeys {
if val, ok := secret.Data[pair.Legacy]; ok {
return string(val)
}
if val, ok := secret.Data[pair.New]; ok {
return string(val)
}
}
return ""
}

// getPMMVersion makes an API request to the PMM server to figure out the current version
func (c *PMMConfig) getPMMVersion(ctx context.Context, auth iAuth, skipTLSVerify bool) (PMMServerVersion, error) {
resp, err := doJSONRequest[struct {
Version string `json:"version"`
}](ctx, http.MethodGet, fmt.Sprintf("%s/v1/version", c.URL), auth, "", skipTLSVerify)
if err != nil {
return "", err
}

return PMMServerVersion(resp.Version), nil
}

// +kubebuilder:object:root=true
Expand Down Expand Up @@ -93,3 +178,58 @@
func init() {
SchemeBuilder.Register(&MonitoringConfig{}, &MonitoringConfigList{})
}

type PMMServerVersion string

Check failure on line 182 in api/everest/v1alpha1/monitoringconfig_types.go

View workflow job for this annotation

GitHub Actions / golangci-lint

exported: exported type PMMServerVersion should have comment or be unexported (revive)

// UsesLegacyAuth returns true if the instance uses legacy auth (PMM2) otherwise it returns false
func (v *PMMServerVersion) UsesLegacyAuth() bool {
ver, err := goversion.NewVersion(string(*v))
if err != nil {
return false
}
segments := ver.Segments()
return len(segments) > 0 && segments[0] == 2
}

func (v *PMMServerVersion) DefaultPMMClientImage() string {

Check failure on line 194 in api/everest/v1alpha1/monitoringconfig_types.go

View workflow job for this annotation

GitHub Actions / golangci-lint

exported: exported method PMMServerVersion.DefaultPMMClientImage should have comment or be unexported (revive)
if v.UsesLegacyAuth() {
return PMM2ClientImage
}
return PMM3ClientImage
}

// PMMSecretKeyName returns the key name that should be used in the PMM secret
// depending on engine and auth type
func (v *PMMServerVersion) PMMSecretKeyName(engineType EngineType) string {
if pair, ok := pmmServerKeys[engineType]; ok {
if v.UsesLegacyAuth() {
return pair.Legacy
}
return pair.New
}
return ""
}

// iAuth an interface to apply auth to a request.
type iAuth interface {
apply(req *http.Request)
}

// basicAuth represents basic auth with User/Password
type basicAuth struct {
user string
password string
}

func (a basicAuth) apply(req *http.Request) {
req.SetBasicAuth(a.user, a.password)
}

// bearerAuth represents bearer auth with a token
type bearerAuth struct {
token string
}

func (a bearerAuth) apply(req *http.Request) {
req.Header.Set("Authorization", "Bearer "+a.token)
}
125 changes: 125 additions & 0 deletions api/everest/v1alpha1/pmm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// everest-operator
// Copyright (C) 2022 Percona LLC
//
// 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 v1alpha1

import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
)

type pmmErrorMessage struct {
Message string `json:"message"`
}

func createKey(ctx context.Context, hostname, apiKeyName string, auth iAuth, skipTLSVerify bool) (string, error) {
body := nameAndRoleMap(apiKeyName)
resp, err := doJSONRequest[struct {
Key string `json:"key"`
}](ctx, http.MethodPost, fmt.Sprintf("%s/graph/api/auth/keys", hostname), auth, body, skipTLSVerify)
if err != nil {
return "", err
}
return resp.Key, nil
}

func createServiceAccountAndToken(ctx context.Context, hostname, apiKeyName string, auth iAuth, skipTLSVerify bool) (string, error) {
// for transparency, use the same name for the generated service account and token
nameAndRole := nameAndRoleMap(apiKeyName)
account, err := doJSONRequest[struct {
Uid string `json:"uid"`

Check failure on line 48 in api/everest/v1alpha1/pmm.go

View workflow job for this annotation

GitHub Actions / golangci-lint

var-naming: struct field Uid should be UID (revive)
}](ctx, http.MethodPost, fmt.Sprintf("%s/graph/api/serviceaccounts", hostname), auth, nameAndRole, skipTLSVerify)
if err != nil {
return "", err
}
token, err := doJSONRequest[struct {
Key string `json:"key"`
}](ctx, http.MethodPost, fmt.Sprintf("%s/graph/api/serviceaccounts/%s/tokens", hostname, account.Uid), auth, nameAndRole, skipTLSVerify)
if err != nil {
return "", err
}

return token.Key, nil
}

// makes an HTTP request using JSON content type
func doJSONRequest[T any](ctx context.Context, method, url string, auth iAuth, body any, skipTLSVerify bool) (T, error) {

Check failure on line 64 in api/everest/v1alpha1/pmm.go

View workflow job for this annotation

GitHub Actions / golangci-lint

doJSONRequest returns generic interface (T) of type param any (ireturn)
var zero T
b, err := json.Marshal(body)
if err != nil {
return zero, fmt.Errorf("marshal request: %w", err)
}

req, err := http.NewRequestWithContext(ctx, method, url, bytes.NewReader(b))
if err != nil {
return zero, fmt.Errorf("build request: %w", err)
}

req.Header.Set("Content-Type", "application/json; charset=utf-8")
if auth != nil {
auth.apply(req)
}
req.Close = true

httpClient := newHTTPClient(skipTLSVerify)
resp, err := httpClient.Do(req)
if err != nil {
return zero, fmt.Errorf("do request: %w", err)
}
defer resp.Body.Close()

Check failure on line 87 in api/everest/v1alpha1/pmm.go

View workflow job for this annotation

GitHub Actions / golangci-lint

Error return value of `resp.Body.Close` is not checked (errcheck)

data, err := io.ReadAll(resp.Body)
if err != nil {
return zero, fmt.Errorf("read response: %w", err)
}

if resp.StatusCode >= http.StatusBadRequest {
var pmmErr *pmmErrorMessage
if err := json.Unmarshal(data, &pmmErr); err != nil {
return zero, errors.Join(err, fmt.Errorf("PMM returned an unknown error. HTTP %d", resp.StatusCode))
}
return zero, fmt.Errorf("PMM returned an error: %s", pmmErr.Message)
}

var result T
if err := json.Unmarshal(data, &result); err != nil {
return zero, fmt.Errorf("unmarshal response: %w", err)
}

return result, nil
}

func nameAndRoleMap(name string) map[string]string {
return map[string]string{
"name": name,
"role": "Admin",
}
}

func newHTTPClient(insecure bool) *http.Client {
client := http.DefaultClient
client.Transport = &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: insecure, //nolint:gosec
},
}
return client
}
2 changes: 1 addition & 1 deletion api/everest/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions bundle/manifests/everest.percona.com_monitoringconfigs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ spec:
observed for this MonitoringConfig.
format: int64
type: integer
pmmServerVersion:
description: PMMServerVersion shows PMM server version
type: string
type: object
type: object
served: true
Expand Down
3 changes: 3 additions & 0 deletions config/crd/bases/everest.percona.com_monitoringconfigs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ spec:
observed for this MonitoringConfig.
format: int64
type: integer
pmmServerVersion:
description: PMMServerVersion shows PMM server version
type: string
type: object
type: object
served: true
Expand Down
3 changes: 3 additions & 0 deletions deploy/bundle.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1890,6 +1890,9 @@ spec:
observed for this MonitoringConfig.
format: int64
type: integer
pmmServerVersion:
description: PMMServerVersion shows PMM server version
type: string
type: object
type: object
served: true
Expand Down
2 changes: 1 addition & 1 deletion docs/guides/crd_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ spec:
credentialsSecretName: pmm-credentials
pmm:
url: "https://pmm.example.com"
image: "percona/pmm-client:2.41.0" # Optional: specify PMM client version
image: "percona/pmm-client:3.4.1" # Optional: specify PMM client version
verifyTLS: true # Optional: verify TLS certificates
```

Expand Down
3 changes: 0 additions & 3 deletions internal/controller/everest/common/pmm_configs.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,6 @@ const (
Kibibyte = 1024
// Mebibyte represents 1 MiB.
Mebibyte = 1024 * Kibibyte

// DefaultPMMClientImage is the default image for PMM client.
DefaultPMMClientImage = "percona/pmm-client:2"
// pmmClientRequestCPUSmall are the default CPU requests for PMM client in small clusters.
pmmClientRequestCPUSmall = 95
// pmmClientRequestCPUMedium are the default CPU requests for PMM client in medium clusters.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ func (r *MonitoringConfigReconciler) Reconcile(ctx context.Context, req ctrl.Req
return ctrl.Result{}, errors.Join(err, fetchErr)
}

credentialsSecret := &corev1.Secret{}

// Update the status and finalizers of the MonitoringConfig object after the reconciliation.
defer func() {
// Nothing to process on delete events
Expand All @@ -99,6 +101,12 @@ func (r *MonitoringConfigReconciler) Reconcile(ctx context.Context, req ctrl.Req

mc.Status.InUse = len(dbList.Items) > 0
mc.Status.LastObservedGeneration = mc.GetGeneration()
v, vErr := mc.GetPMMServerVersion(ctx, credentialsSecret)
if vErr != nil {
logger.Error(err, "Failed to get PMM server version "+vErr.Error())
}
mc.Status.PMMServerVersion = v

if err = r.Client.Status().Update(ctx, mc); err != nil {
rr = ctrl.Result{}
logger.Error(err, fmt.Sprintf("failed to update status for monitoring config='%s'", mcName))
Expand All @@ -123,7 +131,6 @@ func (r *MonitoringConfigReconciler) Reconcile(ctx context.Context, req ctrl.Req
return ctrl.Result{}, r.cleanupSecrets(ctx, mc)
}

credentialsSecret := &corev1.Secret{}
if err := r.Get(ctx, types.NamespacedName{
Name: mc.Spec.CredentialsSecretName,
Namespace: mc.GetNamespace(),
Expand Down
4 changes: 2 additions & 2 deletions internal/controller/everest/providers/pg/applier.go
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,7 @@ func (p *applier) applyPMMCfg(monitoring *everestv1alpha1.MonitoringConfig) erro
Resources: getPMMResources(common.IsNewDatabaseCluster(p.DB.Status.Status),
&p.DB.Spec, &p.currentPGSpec),
Secret: fmt.Sprintf("%s%s-pmm", consts.EverestSecretsPrefix, database.GetName()),
Image: common.DefaultPMMClientImage,
Image: monitoring.Status.PMMServerVersion.DefaultPMMClientImage(),
ImagePullPolicy: p.getPMMImagePullPolicy(),
}

Expand All @@ -399,7 +399,7 @@ func (p *applier) applyPMMCfg(monitoring *everestv1alpha1.MonitoringConfig) erro

if err := common.CreateOrUpdateSecretData(ctx, c, database, pg.Spec.PMM.Secret,
map[string][]byte{
"PMM_SERVER_KEY": []byte(apiKey),
monitoring.Status.PMMServerVersion.PMMSecretKeyName(p.DB.Spec.Engine.Type): []byte(apiKey),
},
true,
); err != nil {
Expand Down
Loading