Skip to content

Commit 90b80df

Browse files
authored
Move cloudid package (#14)
1 parent 7c99c90 commit 90b80df

File tree

3 files changed

+475
-0
lines changed

3 files changed

+475
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
This repository is the home to the common libraries used by Elastic Agent and Beats.
44

55
Provided packages:
6+
* `github.com/elastic/elastic-agent-libs/cloudid` is used for parsing `cloud.id` and `cloud.auth` when connecting to the Elastic stack.
67
* `github.com/elastic/elastic-agent-libs/config` the previous `config.go` file from `github.com/elastic/beats/v7/libbeat/common`. A minimal wrapper around `github.com/elastic/go-ucfg`. It contains helpers for merging and accessing configuration objects and flags.
78
* `github.com/elastic/elastic-agent-libs/file` is responsible for rotating and writing input and output files.
89
* `github.com/elastic/elastic-agent-libs/logp` is the well known logger from libbeat.

cloudid/cloudid.go

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
// Licensed to Elasticsearch B.V. under one or more contributor
2+
// license agreements. See the NOTICE file distributed with
3+
// this work for additional information regarding copyright
4+
// ownership. Elasticsearch B.V. licenses this file to you under
5+
// the Apache License, Version 2.0 (the "License"); you may
6+
// not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
// Package cloudid contains functions for parsing the cloud.id and cloud.auth
19+
// settings and modifying the configuration to take them into account.
20+
package cloudid
21+
22+
import (
23+
"encoding/base64"
24+
"errors"
25+
"fmt"
26+
"net/url"
27+
"strings"
28+
29+
"github.com/elastic/elastic-agent-libs/config"
30+
"github.com/elastic/elastic-agent-libs/logp"
31+
)
32+
33+
const defaultCloudPort = "443"
34+
35+
// CloudID encapsulates the encoded (i.e. raw) and decoded parts of Elastic Cloud ID.
36+
type CloudID struct {
37+
id string
38+
esURL string
39+
kibURL string
40+
41+
auth string
42+
username string
43+
password string
44+
}
45+
46+
// NewCloudID constructs a new CloudID object by decoding the given cloud ID and cloud auth.
47+
func NewCloudID(cloudID string, cloudAuth string) (*CloudID, error) {
48+
cid := CloudID{
49+
id: cloudID,
50+
auth: cloudAuth,
51+
}
52+
53+
if err := cid.decode(); err != nil {
54+
return nil, err
55+
}
56+
57+
return &cid, nil
58+
}
59+
60+
// ElasticsearchURL returns the Elasticsearch URL decoded from the cloud ID.
61+
func (c *CloudID) ElasticsearchURL() string {
62+
return c.esURL
63+
}
64+
65+
// KibanaURL returns the Kibana URL decoded from the cloud ID.
66+
func (c *CloudID) KibanaURL() string {
67+
return c.kibURL
68+
}
69+
70+
// Username returns the username decoded from the cloud auth.
71+
func (c *CloudID) Username() string {
72+
return c.username
73+
}
74+
75+
// Password returns the password decoded from the cloud auth.
76+
func (c *CloudID) Password() string {
77+
return c.password
78+
}
79+
80+
func (c *CloudID) decode() error {
81+
var err error
82+
if err = c.decodeCloudID(); err != nil {
83+
return fmt.Errorf("invalid cloud id '%v': %w", c.id, err)
84+
}
85+
86+
if c.auth != "" {
87+
if err = c.decodeCloudAuth(); err != nil {
88+
return fmt.Errorf("invalid cloud auth: %w", err)
89+
}
90+
}
91+
92+
return nil
93+
}
94+
95+
// decodeCloudID decodes the c.id into c.esURL and c.kibURL
96+
func (c *CloudID) decodeCloudID() error {
97+
cloudID := c.id
98+
99+
// 1. Ignore anything before `:`.
100+
idx := strings.LastIndex(cloudID, ":")
101+
if idx >= 0 {
102+
cloudID = cloudID[idx+1:]
103+
}
104+
105+
// 2. base64 decode
106+
decoded, err := base64.StdEncoding.DecodeString(cloudID)
107+
if err != nil {
108+
return fmt.Errorf("base64 decoding failed on %s: %w", cloudID, err)
109+
}
110+
111+
// 3. separate based on `$`
112+
words := strings.Split(string(decoded), "$")
113+
if len(words) < 3 {
114+
return fmt.Errorf("expected at least 3 parts in %s", string(decoded))
115+
}
116+
117+
// 4. extract port from the ES and Kibana host, or use 443 as the default
118+
host, port := extractPortFromName(words[0], defaultCloudPort)
119+
esID, esPort := extractPortFromName(words[1], port)
120+
kbID, kbPort := extractPortFromName(words[2], port)
121+
122+
// 5. form the URLs
123+
esURL := url.URL{Scheme: "https", Host: fmt.Sprintf("%s.%s:%s", esID, host, esPort)}
124+
kibanaURL := url.URL{Scheme: "https", Host: fmt.Sprintf("%s.%s:%s", kbID, host, kbPort)}
125+
126+
c.esURL = esURL.String()
127+
c.kibURL = kibanaURL.String()
128+
129+
return nil
130+
}
131+
132+
// decodeCloudAuth splits the c.auth into c.username and c.password.
133+
func (c *CloudID) decodeCloudAuth() error {
134+
cloudAuth := c.auth
135+
idx := strings.Index(cloudAuth, ":")
136+
if idx < 0 {
137+
return errors.New("cloud.auth setting doesn't contain `:` to split between username and password")
138+
}
139+
140+
c.username = cloudAuth[0:idx]
141+
c.password = cloudAuth[idx+1:]
142+
return nil
143+
}
144+
145+
// OverwriteSettings modifies the received config object by overwriting the
146+
// output.elasticsearch.hosts, output.elasticsearch.username, output.elasticsearch.password,
147+
// setup.kibana.host settings based on values derived from the cloud.id and cloud.auth
148+
// settings.
149+
func OverwriteSettings(cfg *config.C) error {
150+
151+
logger := logp.NewLogger("cloudid")
152+
cloudID, _ := cfg.String("cloud.id", -1)
153+
cloudAuth, _ := cfg.String("cloud.auth", -1)
154+
155+
if cloudID == "" && cloudAuth == "" {
156+
// nothing to hack
157+
return nil
158+
}
159+
160+
logger.Debugf("cloud.id: %s, cloud.auth: %s", cloudID, cloudAuth)
161+
if cloudID == "" {
162+
return errors.New("cloud.auth specified but cloud.id is empty. Please specify both")
163+
}
164+
165+
// cloudID overwrites
166+
cid, err := NewCloudID(cloudID, cloudAuth)
167+
if err != nil {
168+
return fmt.Errorf("error decoding cloud.id: %w", err)
169+
}
170+
171+
logger.Infof("Setting Elasticsearch and Kibana URLs based on the cloud id: output.elasticsearch.hosts=%s and setup.kibana.host=%s", cid.esURL, cid.kibURL)
172+
173+
esURLConfig, err := config.NewConfigFrom([]string{cid.ElasticsearchURL()})
174+
if err != nil {
175+
return err
176+
}
177+
178+
// Before enabling the ES output, check that no other output is enabled
179+
tmp := struct {
180+
Output config.Namespace `config:"output"`
181+
}{}
182+
if err := cfg.Unpack(&tmp); err != nil {
183+
return err
184+
}
185+
if out := tmp.Output; out.IsSet() && out.Name() != "elasticsearch" {
186+
return fmt.Errorf("the cloud.id setting enables the Elasticsearch output, but you already have the %s output enabled in the config", out.Name())
187+
}
188+
189+
err = cfg.SetChild("output.elasticsearch.hosts", -1, esURLConfig)
190+
if err != nil {
191+
return err
192+
}
193+
194+
err = cfg.SetString("setup.kibana.host", -1, cid.KibanaURL())
195+
if err != nil {
196+
return err
197+
}
198+
199+
if cloudAuth != "" {
200+
// cloudAuth overwrites
201+
err = cfg.SetString("output.elasticsearch.username", -1, cid.Username())
202+
if err != nil {
203+
return err
204+
}
205+
206+
err = cfg.SetString("output.elasticsearch.password", -1, cid.Password())
207+
if err != nil {
208+
return err
209+
}
210+
}
211+
212+
return nil
213+
}
214+
215+
// extractPortFromName takes a string in the form `id:port` and returns the
216+
// ID and the port. If there's no `:`, the default port is returned
217+
func extractPortFromName(word string, defaultPort string) (id, port string) {
218+
idx := strings.LastIndex(word, ":")
219+
if idx >= 0 {
220+
return word[:idx], word[idx+1:]
221+
}
222+
return word, defaultPort
223+
}

0 commit comments

Comments
 (0)