forked from openshift/operator-framework-olm
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This is the 1st commit message: wip This is the commit message openshift#2:
- Loading branch information
Showing
11 changed files
with
624 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,262 @@ | ||
package cmd | ||
|
||
import ( | ||
"bytes" | ||
"crypto/tls" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"net/url" | ||
"os" | ||
"path/filepath" | ||
"strings" | ||
|
||
"github.com/spf13/cobra" | ||
corev1 "k8s.io/api/core/v1" | ||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
"k8s.io/klog/v2" | ||
"sigs.k8s.io/controller-runtime/pkg/client" | ||
|
||
"github.com/openshift/operator-framework-olm/pkg/profiling/config" | ||
"github.com/openshift/operator-framework-olm/pkg/version" | ||
) | ||
|
||
const ( | ||
profileConfigMapLabelKey = "olm.openshift.io/pprof" | ||
) | ||
|
||
var ( | ||
rootCmd = newCmd() | ||
|
||
// Used for flags | ||
namespace string | ||
configPath string | ||
clientCAPath string | ||
) | ||
|
||
func init() { | ||
rootCmd.PersistentFlags().StringVarP(&namespace, "namespace", "n", "default", "The Kubernetes namespace where the generated configMaps should exist. Defaults to \"default\".") | ||
rootCmd.MarkFlagRequired("namespace") | ||
rootCmd.PersistentFlags().StringVarP(&configPath, "config-path", "c", "/etc/config/config.yaml", "The path to the collect-profiles configuration file.") | ||
rootCmd.MarkFlagRequired("config-file") | ||
rootCmd.PersistentFlags().StringVarP(&clientCAPath, "client-ca", "", "/etc/pki/tls/certs/", "The path to the tls cert used by the client making https requests against the pprof endpoints.") | ||
} | ||
|
||
func Execute() { | ||
if err := rootCmd.Execute(); err != nil { | ||
klog.Fatal(err) | ||
os.Exit(1) | ||
} | ||
} | ||
|
||
func getTruePointer() *bool { | ||
trueBool := true | ||
return &trueBool | ||
} | ||
|
||
func newCmd() *cobra.Command { | ||
var cfg config.Configuration | ||
return &cobra.Command{ | ||
Use: "collect-profiles endpoint:argument", | ||
Short: "Retrieve the pprof data from an endpoint and stores it in a configMap", | ||
Long: `The collect-profiles command makes https requests against pprof endpoints | ||
provided as arguments and stores that information in immutable configMaps.`, | ||
Version: version.String(), | ||
PersistentPreRunE: func(*cobra.Command, []string) error { | ||
return cfg.Load() | ||
}, | ||
RunE: func(cmd *cobra.Command, args []string) error { | ||
if len(args) < 1 { | ||
return fmt.Errorf("must specify endpoint") | ||
} | ||
|
||
jobConfig, err := config.GetConfig(configPath) | ||
if err != nil { | ||
klog.Infof("error retrieving job config") | ||
return err | ||
} | ||
|
||
// Exit if job is disabled | ||
if jobConfig.Disabled { | ||
klog.Infof("CronJob disabled, exiting") | ||
return nil | ||
} | ||
|
||
// Validate input | ||
validatedArguments := make([]*argument, len(args)) | ||
for i, arg := range args { | ||
a, err := newArgument(arg) | ||
if err != nil { | ||
return err | ||
} | ||
validatedArguments[i] = a | ||
} | ||
|
||
// Get existing configmaps | ||
existingConfigMaps := &corev1.ConfigMapList{} | ||
if err := cfg.Client.List(cmd.Context(), existingConfigMaps, client.InNamespace(namespace), client.HasLabels{profileConfigMapLabelKey}); err != nil { | ||
return err | ||
} | ||
|
||
newestConfigMaps, expiredConfigMaps := separateConfigMapsIntoNewestAndExpired(existingConfigMaps.Items) | ||
|
||
// Attempt to delete all but the newest configMaps generated by this job | ||
errs := []error{} | ||
for _, cm := range expiredConfigMaps { | ||
if err := cfg.Client.Delete(cmd.Context(), &cm); err != nil { | ||
errs = append(errs, err) // log the delete error | ||
continue | ||
} | ||
klog.Infof("Successfully deleted configMap %s/%s", cm.GetNamespace(), cm.GetName()) | ||
} | ||
|
||
// If a delete call failed, abort to avoid creating new configMaps | ||
if len(errs) != 0 { | ||
return fmt.Errorf("error deleting expired pprof configMaps: %v", errs) | ||
} | ||
|
||
httpClient, err := getHttpClient(clientCAPath) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
// Track successfully created configMaps by generateName for each endpoint being scrapped. | ||
createdCM := map[string]struct{}{} | ||
|
||
for _, a := range validatedArguments { | ||
b, err := requestURLBody(httpClient, a.url) | ||
if err != nil { | ||
klog.Infof("error retrieving pprof profile: %v", err) | ||
continue | ||
} | ||
|
||
cm := &corev1.ConfigMap{ | ||
ObjectMeta: metav1.ObjectMeta{ | ||
GenerateName: a.generateName, | ||
Namespace: namespace, | ||
Labels: map[string]string{ | ||
profileConfigMapLabelKey: "", | ||
}, | ||
}, | ||
Immutable: getTruePointer(), | ||
BinaryData: map[string][]byte{ | ||
"profile.pb.gz": b, | ||
}, | ||
} | ||
|
||
if err := cfg.Client.Create(cmd.Context(), cm); err != nil { | ||
klog.Errorf("error created configMap %s/%s: %v", cm.GetNamespace(), cm.GetName(), err) | ||
continue | ||
} | ||
|
||
klog.Infof("Successfully created configMap %s/%s", cm.GetNamespace(), cm.GetName()) | ||
createdCM[a.generateName] = struct{}{} | ||
} | ||
|
||
// Delete the configMaps which are no longer the newest | ||
for _, cm := range newestConfigMaps { | ||
// Don't delete ConfigMaps that were not replaced | ||
// Also prevents deletes of configMaps with generateNames not included in command. | ||
if _, ok := createdCM[cm.GenerateName]; !ok { | ||
continue | ||
} | ||
if err := cfg.Client.Delete(cmd.Context(), &cm); err != nil { | ||
errs = append(errs, err) | ||
continue | ||
} | ||
klog.Infof("Successfully deleted configMap %s/%s", cm.GetNamespace(), cm.GetName()) | ||
} | ||
|
||
if len(errs) != 0 { | ||
return fmt.Errorf("error deleting existing pprof configMaps: %v", errs) | ||
} | ||
return nil | ||
}, | ||
} | ||
} | ||
|
||
func separateConfigMapsIntoNewestAndExpired(configMaps []corev1.ConfigMap) (newestCMs []corev1.ConfigMap, expiredCMs []corev1.ConfigMap) { | ||
// Group ConfigMaps by GenerateName | ||
newestConfigMaps := map[string]corev1.ConfigMap{} | ||
for _, cm := range configMaps { | ||
if _, ok := newestConfigMaps[cm.GenerateName]; !ok { | ||
newestConfigMaps[cm.GenerateName] = cm | ||
continue | ||
} | ||
if cm.CreationTimestamp.After(newestConfigMaps[cm.GenerateName].CreationTimestamp.Time) { | ||
newestConfigMaps[cm.GenerateName], cm = cm, newestConfigMaps[cm.GenerateName] | ||
} | ||
expiredCMs = append(expiredCMs, cm) | ||
} | ||
|
||
for _, v := range newestConfigMaps { | ||
newestCMs = append(newestCMs, v) | ||
} | ||
|
||
return newestCMs, expiredCMs | ||
} | ||
|
||
type argument struct { | ||
generateName string | ||
url *url.URL | ||
} | ||
|
||
func newArgument(s string) (*argument, error) { | ||
splitStrings := strings.SplitN(s, ":", 2) | ||
if len(splitStrings) != 2 { | ||
return nil, fmt.Errorf("Error") | ||
} | ||
|
||
url, err := url.Parse(splitStrings[1]) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
if strings.ToLower(url.Scheme) != "https" { | ||
return nil, fmt.Errorf("URL Scheme must be HTTPS") | ||
} | ||
|
||
arg := &argument{ | ||
generateName: splitStrings[0], | ||
url: url, | ||
} | ||
|
||
return arg, nil | ||
} | ||
|
||
func getHttpClient(clientCAPath string) (*http.Client, error) { | ||
cert, err := tls.LoadX509KeyPair(filepath.Join(clientCAPath, corev1.TLSCertKey), filepath.Join(clientCAPath, corev1.TLSPrivateKeyKey)) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return &http.Client{ | ||
Transport: &http.Transport{ | ||
TLSClientConfig: &tls.Config{ | ||
InsecureSkipVerify: true, | ||
Certificates: []tls.Certificate{cert}, | ||
}, | ||
}, | ||
}, nil | ||
} | ||
|
||
func requestURLBody(httpClient *http.Client, u *url.URL) ([]byte, error) { | ||
response, err := httpClient.Do(&http.Request{ | ||
Method: http.MethodGet, | ||
URL: u, | ||
}) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
if response.StatusCode != http.StatusOK { | ||
return nil, fmt.Errorf("%s responded with %d status code instead of %d", u, response.StatusCode, http.StatusOK) | ||
} | ||
|
||
var b bytes.Buffer | ||
if _, err := io.Copy(&b, response.Body); err != nil { | ||
return nil, fmt.Errorf("error reading response body: %v", err) | ||
} | ||
|
||
return b.Bytes(), nil | ||
} |
Oops, something went wrong.