systemtest/kibana.go (231 lines of code) (raw):

// Licensed to Elasticsearch B.V. under one or more contributor // license agreements. See the NOTICE file distributed with // this work for additional information regarding copyright // ownership. Elasticsearch B.V. licenses this file to you 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 systemtest import ( "bytes" "encoding/json" "errors" "fmt" "io" "log" "mime/multipart" "net/http" "net/url" "path" "testing" "time" "github.com/stretchr/testify/require" "gopkg.in/yaml.v3" "github.com/elastic/apm-server/systemtest/apmservertest" "github.com/elastic/apm-server/systemtest/estest" "github.com/elastic/apm-server/systemtest/fleettest" "github.com/elastic/apm-tools/pkg/espoll" ) const ( adminKibanaUser = adminElasticsearchUser adminKibanaPass = adminElasticsearchPass // agentPolicyDescription holds the description associated with agent // policies created by CreateAgentPolicy. This can be used in filters. agentPolicyDescription = "apm_systemtest" ) var ( // KibanaURL is the base URL for Kibana, including userinfo for // authenticating as the admin user. KibanaURL *url.URL // Fleet is a Fleet API client for use in tests. Fleet *fleettest.Client // IntegrationPackage holds the "apm" integration package details. // // IntegrationPackage is initialised by InitFleetPackage. IntegrationPackage *fleettest.Package ) func initKibana() { kibanaConfig := apmservertest.DefaultConfig().Kibana u, err := url.Parse(kibanaConfig.Host) if err != nil { log.Fatal(err) } u.User = url.UserPassword(adminKibanaUser, adminKibanaPass) KibanaURL = u Fleet = fleettest.NewClient(KibanaURL.String()) } // InitFleet ensures Fleet is set up, destroys any existing agent policies previously // created by the system tests and unenrolls the associated agents. After InitFleet // returns successfully, the IntegrationPackage var will be initialised to the details // of the installed APM integration package. func InitFleet() error { if err := Fleet.Setup(); err != nil { log.Fatal(err) } agentPolicies, err := Fleet.AgentPolicies("ingest-agent-policies.description:" + agentPolicyDescription) if err != nil { return err } ids := make([]string, len(agentPolicies)) for i, agentPolicy := range agentPolicies { ids[i] = agentPolicy.ID } if err := DestroyAgentPolicy(ids...); err != nil { return fmt.Errorf("failed to destroy agent policy: %w", err) } return InitFleetPackage() } // InitFleetPackage and sets IntegrationPackage to the details of the installed // APM integration package. InitFleetPackage assumes that Fleet has been set up // already. func InitFleetPackage() error { packages, err := Fleet.ListPackages() if err != nil { return err } for _, pkg := range packages { if pkg.Name != "apm" { continue } IntegrationPackage = &pkg return nil } return errors.New("'apm' integration package not installed") } // CreateAgentPolicy creates an Agent policy with the given name and namespace, // creates an APM package policy with the provided config vars, and assigns it // to the agent policy. // // The agent policy will be destroyed, and any assigned agents unenrolled, when // the test completes. // // This should typically be used by tests instead of directly calling the // fleettest.Client.CreateAgentPolicy method. func CreateAgentPolicy(t testing.TB, name, namespace string, vars, config map[string]interface{}) (*fleettest.AgentPolicy, *fleettest.EnrollmentAPIKey) { agentPolicy, key, err := Fleet.CreateAgentPolicy(name, namespace, agentPolicyDescription) require.NoError(t, err) t.Cleanup(func() { err := DestroyAgentPolicy(agentPolicy.ID) require.NoError(t, err) }) packagePolicy := NewPackagePolicy(agentPolicy, vars, config) err = Fleet.CreatePackagePolicy(packagePolicy) require.NoError(t, err) return agentPolicy, key } // DestroyAgentPolicy deletes the agent policies with given IDs, // and bulk unenrolls the agents assigned to them. func DestroyAgentPolicy(id ...string) error { if len(id) == 0 { return nil } agents, err := Fleet.Agents() if err != nil { return err } agentsByPolicy := make(map[string][]fleettest.Agent) for _, agent := range agents { agentsByPolicy[agent.PolicyID] = append(agentsByPolicy[agent.PolicyID], agent) } for _, agentPolicyID := range id { if agents := agentsByPolicy[agentPolicyID]; len(agents) > 0 { agentIDs := make([]string, len(agents)) for i, agent := range agents { agentIDs[i] = agent.ID } if err := Fleet.BulkUnenrollAgents(true, agentIDs...); err != nil { return err } } if err := Fleet.DeleteAgentPolicy(agentPolicyID); err != nil { return err } } return nil } // NewPackagePolicy returns a new fleettest.PackagePolicy with config vars, but does not create it. // // The returned package policy is suitable for passing to Fleet.CreatePackagePolicy. func NewPackagePolicy(agentPolicy *fleettest.AgentPolicy, varValues, extraConfig map[string]interface{}) *fleettest.PackagePolicy { // Package policy names must be globally unique. We generate unique agent // policy names, so just append the package name to that. packagePolicyName := agentPolicy.Name + "-apm" packagePolicy := fleettest.NewPackagePolicy(IntegrationPackage, packagePolicyName, agentPolicy.Namespace, agentPolicy.ID) packagePolicy.Package.Name = IntegrationPackage.Name packagePolicy.Package.Version = IntegrationPackage.Version packagePolicy.Package.Title = IntegrationPackage.Title for _, input := range IntegrationPackage.PolicyTemplates[0].Inputs { vars := make(map[string]interface{}) for _, inputVar := range input.Vars { value, ok := varValues[inputVar.Name] if !ok { value = inputVarDefault(inputVar) } varMap := map[string]interface{}{"type": inputVar.Type} if value != nil { if inputVar.Type == "yaml" { encoded, err := json.Marshal(value) if err != nil { panic(err) } value = string(encoded) } varMap["value"] = value } vars[inputVar.Name] = varMap } packagePolicy.Inputs = append(packagePolicy.Inputs, fleettest.PackagePolicyInput{ Type: input.Type, Enabled: true, Streams: []interface{}{}, Vars: vars, Config: extraConfig, }) } return packagePolicy } func inputVarDefault(inputVar fleettest.PackagePolicyTemplateInputVar) interface{} { if inputVar.Name == "host" { return ":8200" } if inputVar.Default != nil { defaultValue := inputVar.Default if inputVar.Type == "yaml" { var v interface{} if err := yaml.Unmarshal([]byte(defaultValue.(string)), &v); err != nil { panic(err) } defaultValue = v } return defaultValue } if inputVar.Multi { return []interface{}{} } return nil } // SourceMap holds information about a source map stored by Kibana. type SourceMap struct { ID string `json:"id"` Created time.Time `json:"created"` Body map[string]interface{} `json:"body"` } // CreateSourceMap creates or replaces a source map with the given service name // and version, and bundle filepath. CreateSourceMap returns the ID of the stored // source map, which may be passed to DeleteSourceMap for cleanup. func CreateSourceMap(t testing.TB, sourcemap []byte, serviceName, serviceVersion, bundleFilepath string) string { t.Helper() var data bytes.Buffer mw := multipart.NewWriter(&data) require.NoError(t, mw.WriteField("service_name", serviceName)) require.NoError(t, mw.WriteField("service_version", serviceVersion)) require.NoError(t, mw.WriteField("bundle_filepath", bundleFilepath)) sourcemapFileWriter, err := mw.CreateFormFile("sourcemap", "sourcemap.js.map") require.NoError(t, err) sourcemapFileWriter.Write(sourcemap) require.NoError(t, mw.Close()) apiURL := *KibanaURL apiURL.Path += "/api/apm/sourcemaps" req, _ := http.NewRequest("POST", apiURL.String(), &data) req.Header.Add("Content-Type", mw.FormDataContentType()) req.Header.Set("kbn-xsrf", "1") resp, err := http.DefaultClient.Do(req) require.NoError(t, err) defer resp.Body.Close() respBody, err := io.ReadAll(resp.Body) require.NoError(t, err) require.Equal(t, http.StatusOK, resp.StatusCode, string(respBody)) var result struct { ID string `json:"id"` } err = json.Unmarshal(respBody, &result) require.NoError(t, err) cleanPath := bundleFilepath u, err := url.Parse(bundleFilepath) if err == nil { u.Fragment = "" u.RawQuery = "" u.Path = path.Clean(u.Path) cleanPath = u.String() } id := serviceName + "-" + serviceVersion + "-" + cleanPath estest.ExpectMinDocs(t, Elasticsearch, 1, ".apm-source-map", espoll.TermQuery{ Field: "_id", Value: id, }) t.Cleanup(func() { DeleteSourceMap(t, result.ID) }) return result.ID } // DeleteSourceMap deletes a source map with the given ID. func DeleteSourceMap(t testing.TB, id string) { t.Helper() url := *KibanaURL url.Path += "/api/apm/sourcemaps/" + id req, _ := http.NewRequest("DELETE", url.String(), nil) req.Header.Set("kbn-xsrf", "1") resp, err := http.DefaultClient.Do(req) require.NoError(t, err) defer resp.Body.Close() respBody, err := io.ReadAll(resp.Body) require.NoError(t, err) require.Equal(t, http.StatusOK, resp.StatusCode, string(respBody)) }