From b1e5c4f25793f68dee937f3383f3c951fd013bde Mon Sep 17 00:00:00 2001 From: andsel Date: Wed, 11 Nov 2020 13:56:55 +0100 Subject: [PATCH] Moved CloudSettingId class and tests to Java (cherry picked from commit 2545fa45ba0eca9adec2e8ce87bc428d80a342ab) --- .../lib/logstash/util/cloud_setting_id.rb | 89 +------ .../spec/logstash/settings/modules_spec.rb | 8 +- .../logstash/util/cloud_setting_id_spec.rb | 218 ------------------ .../org/logstash/util/CloudSettingId.java | 191 +++++++++++++++ .../org/logstash/util/CloudSettingIdTest.java | 194 ++++++++++++++++ 5 files changed, 392 insertions(+), 308 deletions(-) delete mode 100644 logstash-core/spec/logstash/util/cloud_setting_id_spec.rb create mode 100644 logstash-core/src/main/java/org/logstash/util/CloudSettingId.java create mode 100644 logstash-core/src/test/java/org/logstash/util/CloudSettingIdTest.java diff --git a/logstash-core/lib/logstash/util/cloud_setting_id.rb b/logstash-core/lib/logstash/util/cloud_setting_id.rb index 0a47bdbc8..5fb35c3d3 100644 --- a/logstash-core/lib/logstash/util/cloud_setting_id.rb +++ b/logstash-core/lib/logstash/util/cloud_setting_id.rb @@ -17,89 +17,6 @@ require "base64" -module LogStash module Util class CloudSettingId - - def self.cloud_id_encode(*args) - Base64.urlsafe_encode64(args.join("$")) - end - DOT_SEPARATOR = "." - CLOUD_PORT = "443" - - attr_reader :original, :decoded, :label - attr_reader :elasticsearch_host, :elasticsearch_scheme, :elasticsearch_port - attr_reader :kibana_host, :kibana_scheme, :kibana_port - attr_reader :other_identifiers - - # The constructor is expecting a 'cloud.id', a string in 2 variants. - # 1 part example: 'dXMtZWFzdC0xLmF3cy5mb3VuZC5pbyRub3RhcmVhbCRpZGVudGlmaWVy' - # 2 part example: 'foobar:dXMtZWFzdC0xLmF3cy5mb3VuZC5pbyRub3RhcmVhbCRpZGVudGlmaWVy' - # The two part variant has a 'label' prepended with a colon separator. The label is not encoded. - # The 1 part (or second section of the 2 part variant) is base64 encoded. - # The original string before encoding has three segments separated by a dollar sign. - # e.g. 'us-east-1.aws.found.io$notareal$identifier' - # The first segment is the cloud base url, e.g. 'us-east-1.aws.found.io' - # The second segment is the elasticsearch host identifier, e.g. 'notareal' - # The third segment is the kibana host identifier, e.g. 'identifier' - # The 'cloud.id' value decoded into the #attr_reader ivars. - def initialize(value) - return if value.nil? - - unless value.is_a?(String) - raise ArgumentError.new("Cloud Id must be String. Received: #{value.class}") - end - @original = value - @label, colon, encoded = @original.partition(":") - if encoded.empty? - @decoded = Base64.urlsafe_decode64(@label) rescue "" - @label = "" - else - @decoded = Base64.urlsafe_decode64(encoded) rescue "" - end - - @decoded = @decoded.encode(Encoding::UTF_8, :invalid => :replace, :undef => :replace) - - if @decoded.count("$") < 2 - raise ArgumentError.new("Cloud Id, after decoding, is invalid. Format: '$$'. Received: \"#{@decoded}\".") - end - - segments = @decoded.split("$") - if segments.any?(&:empty?) - raise ArgumentError.new("Cloud Id, after decoding, is invalid. Format: '$$'. Received: \"#{@decoded}\".") - end - cloud_base = segments.shift - cloud_host = "#{DOT_SEPARATOR}#{cloud_base}" - cloud_host, cloud_port = cloud_host.split(":") - cloud_port ||= CLOUD_PORT - - @elasticsearch_host, @kibana_host, *@other_identifiers = segments - @elasticsearch_host, @elasticsearch_port = @elasticsearch_host.split(":") - @kibana_host, @kibana_port = @kibana_host.split(":") if @kibana_host - @elasticsearch_port ||= cloud_port - @kibana_port ||= cloud_port - @other_identifiers ||= [] - - if @elasticsearch_host == "undefined" - raise ArgumentError.new("Cloud Id, after decoding, elasticsearch segment is 'undefined', literally.") - end - @elasticsearch_scheme = "https" - @elasticsearch_host.concat(cloud_host) - @elasticsearch_host.concat(":#{@elasticsearch_port}") - - if @kibana_host == "undefined" - raise ArgumentError.new("Cloud Id, after decoding, the kibana segment is 'undefined', literally. You may need to enable Kibana in the Cloud UI.") - end - - @kibana_scheme = "https" - @kibana_host ||= String.new # non-sense really to have '.my-host:443' but we're mirroring others - @kibana_host.concat(cloud_host) - @kibana_host.concat(":#{@kibana_port}") - end - - def to_s - @decoded.to_s - end - - def inspect - to_s - end -end end end +module LogStash; module Util + java_import org.logstash.util.CloudSettingId +end; end diff --git a/logstash-core/spec/logstash/settings/modules_spec.rb b/logstash-core/spec/logstash/settings/modules_spec.rb index dd3404c64..1999e7232 100644 --- a/logstash-core/spec/logstash/settings/modules_spec.rb +++ b/logstash-core/spec/logstash/settings/modules_spec.rb @@ -50,26 +50,26 @@ describe LogStash::Setting::Modules do subject { described_class.new("mycloudid", LogStash::Util::CloudSettingId) } context "when given a string which is not a cloud id" do it "should raise an exception" do - expect { subject.set("foobarbaz") }.to raise_error(ArgumentError, /Cloud Id.*is invalid/) + expect { subject.set("foobarbaz") }.to raise_error(IllegalArgumentException, /Cloud Id.*is invalid/) end end context "when given a string which is empty" do it "should raise an exception" do - expect { subject.set("") }.to raise_error(ArgumentError, /Cloud Id.*is invalid/) + expect { subject.set("") }.to raise_error(IllegalArgumentException, /Cloud Id.*is invalid/) end end context "when given a string which is has environment prefix only" do it "should raise an exception" do - expect { subject.set("testing:") }.to raise_error(ArgumentError, /Cloud Id.*is invalid/) + expect { subject.set("testing:") }.to raise_error(IllegalArgumentException, /Cloud Id.*is invalid/) end end context "when given a badly formatted encoded id" do it "should not raise an error" do encoded = Base64.urlsafe_encode64("foo$$bal") - expect { subject.set(encoded) }.to raise_error(ArgumentError, "Cloud Id, after decoding, is invalid. Format: '$$'. Received: \"foo$$bal\".") + expect { subject.set(encoded) }.to raise_error(IllegalArgumentException, "Cloud Id, after decoding, is invalid. Format: '$$'. Received: \"foo$$bal\".") end end diff --git a/logstash-core/spec/logstash/util/cloud_setting_id_spec.rb b/logstash-core/spec/logstash/util/cloud_setting_id_spec.rb deleted file mode 100644 index 9f2d397cc..000000000 --- a/logstash-core/spec/logstash/util/cloud_setting_id_spec.rb +++ /dev/null @@ -1,218 +0,0 @@ -# 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. - -require "spec_helper" -require "logstash/util/cloud_setting_id" - -describe LogStash::Util::CloudSettingId do - let(:input) { "foobar:dXMtZWFzdC0xLmF3cy5mb3VuZC5pbyRub3RhcmVhbCRpZGVudGlmaWVy" } - subject { described_class.new(input) } - - describe "when given unacceptable input" do - it "a nil input does not raise an exception" do - expect{described_class.new(nil)}.not_to raise_exception - end - it "when given a nil input, the accessors are all nil" do - cloud_id = described_class.new(nil) - expect(cloud_id.original).to be_nil - expect(cloud_id.decoded).to be_nil - expect(cloud_id.label).to be_nil - expect(cloud_id.elasticsearch_host).to be_nil - expect(cloud_id.kibana_host).to be_nil - expect(cloud_id.elasticsearch_scheme).to be_nil - expect(cloud_id.kibana_scheme).to be_nil - end - - context "when a malformed value is given" do - let(:raw) {%w(first second)} - let(:input) { described_class.cloud_id_encode(*raw) } - it "raises an error" do - expect{subject}.to raise_exception(ArgumentError, "Cloud Id, after decoding, is invalid. Format: '$$'. Received: \"#{raw[0]}$#{raw[1]}\".") - end - end - - context "when at least one segment is empty" do - let(:raw) {["first", "", "third"]} - let(:input) { described_class.cloud_id_encode(*raw) } - it "raises an error" do - expect{subject}.to raise_exception(ArgumentError, "Cloud Id, after decoding, is invalid. Format: '$$'. Received: \"#{raw[0]}$#{raw[1]}$#{raw[2]}\".") - end - end - - context "when elasticsearch segment is undefined" do - let(:raw) {%w(us-east-1.aws.found.io undefined my-kibana)} - let(:input) { described_class.cloud_id_encode(*raw) } - it "raises an error" do - expect{subject}.to raise_exception(ArgumentError, "Cloud Id, after decoding, elasticsearch segment is 'undefined', literally.") - end - end - - context "when kibana segment is undefined" do - let(:raw) {%w(us-east-1.aws.found.io my-elastic-cluster undefined)} - let(:input) { described_class.cloud_id_encode(*raw) } - it "raises an error" do - expect{subject}.to raise_exception(ArgumentError, "Cloud Id, after decoding, the kibana segment is 'undefined', literally. You may need to enable Kibana in the Cloud UI.") - end - end - end - - describe "without a label" do - let(:input) { "dXMtZWFzdC0xLmF3cy5mb3VuZC5pbyRub3RhcmVhbCRpZGVudGlmaWVy" } - it "#label is empty" do - expect(subject.label).to be_empty - end - it "#decode is set" do - expect(subject.decoded).to eq("us-east-1.aws.found.io$notareal$identifier") - end - end - - describe "when given acceptable input, the accessors:" do - it '#original has a value' do - expect(subject.original).to eq(input) - end - it '#decoded has a value' do - expect(subject.decoded).to eq("us-east-1.aws.found.io$notareal$identifier") - end - it '#label has a value' do - expect(subject.label).to eq("foobar") - end - it '#elasticsearch_host has a value' do - expect(subject.elasticsearch_host).to eq("notareal.us-east-1.aws.found.io:443") - end - it '#elasticsearch_scheme has a value' do - expect(subject.elasticsearch_scheme).to eq("https") - end - it '#kibana_host has a value' do - expect(subject.kibana_host).to eq("identifier.us-east-1.aws.found.io:443") - end - it '#kibana_scheme has a value' do - expect(subject.kibana_scheme).to eq("https") - end - it '#to_s has a value of #decoded' do - expect(subject.to_s).to eq(subject.decoded) - end - end - context "when cloud id contains port descriptions for ES and Kibana" do - let(:input) { "different-es-kb-port:dXMtY2VudHJhbDEuZ2NwLmNsb3VkLmVzLmlvJGFjMzFlYmI5MDI0MTc3MzE1NzA0M2MzNGZkMjZmZDQ2OjkyNDMkYTRjMDYyMzBlNDhjOGZjZTdiZTg4YTA3NGEzYmIzZTA6OTI0NA==" } - - it "decodes the elasticsearch port corretly" do - expect(subject.elasticsearch_host).to eq("ac31ebb90241773157043c34fd26fd46.us-central1.gcp.cloud.es.io:9243") - end - it "decodes the kibana port corretly" do - expect(subject.kibana_host).to eq("a4c06230e48c8fce7be88a074a3bb3e0.us-central1.gcp.cloud.es.io:9244") - end - end - context "when cloud id contains cloud port" do - let(:input) { "custom-port:dXMtY2VudHJhbDEuZ2NwLmNsb3VkLmVzLmlvOjkyNDMkYWMzMWViYjkwMjQxNzczMTU3MDQzYzM0ZmQyNmZkNDYkYTRjMDYyMzBlNDhjOGZjZTdiZTg4YTA3NGEzYmIzZTA=" } - - it "decodes the elasticsearch port corretly" do - expect(subject.elasticsearch_host).to eq("ac31ebb90241773157043c34fd26fd46.us-central1.gcp.cloud.es.io:9243") - end - it "decodes the kibana port corretly" do - expect(subject.kibana_host).to eq("a4c06230e48c8fce7be88a074a3bb3e0.us-central1.gcp.cloud.es.io:9243") - end - end - context "when cloud id only defines kibana port" do - let(:input) { "only-kb-set:dXMtY2VudHJhbDEuZ2NwLmNsb3VkLmVzLmlvJGFjMzFlYmI5MDI0MTc3MzE1NzA0M2MzNGZkMjZmZDQ2JGE0YzA2MjMwZTQ4YzhmY2U3YmU4OGEwNzRhM2JiM2UwOjkyNDQ=" } - - it "defaults the elasticsearch port to 443" do - expect(subject.elasticsearch_host).to eq("ac31ebb90241773157043c34fd26fd46.us-central1.gcp.cloud.es.io:443") - end - it "decodes the kibana port corretly" do - expect(subject.kibana_host).to eq("a4c06230e48c8fce7be88a074a3bb3e0.us-central1.gcp.cloud.es.io:9244") - end - end - context "when cloud id defines cloud port and kibana port" do - let(:input) { "host-and-kb-set:dXMtY2VudHJhbDEuZ2NwLmNsb3VkLmVzLmlvOjkyNDMkYWMzMWViYjkwMjQxNzczMTU3MDQzYzM0ZmQyNmZkNDYkYTRjMDYyMzBlNDhjOGZjZTdiZTg4YTA3NGEzYmIzZTA6OTI0NA==" } - - it "sets the elasticsearch port to cloud port" do - expect(subject.elasticsearch_host).to eq("ac31ebb90241773157043c34fd26fd46.us-central1.gcp.cloud.es.io:9243") - end - it "overrides cloud port with the kibana port" do - expect(subject.kibana_host).to eq("a4c06230e48c8fce7be88a074a3bb3e0.us-central1.gcp.cloud.es.io:9244") - end - end - context "when cloud id defines extra data" do - let(:input) { "extra-items:dXMtY2VudHJhbDEuZ2NwLmNsb3VkLmVzLmlvJGFjMzFlYmI5MDI0MTc3MzE1NzA0M2MzNGZkMjZmZDQ2JGE0YzA2MjMwZTQ4YzhmY2U3YmU4OGEwNzRhM2JiM2UwJGFub3RoZXJpZCRhbmRhbm90aGVy" } - - it "captures the elasticsearch host" do - expect(subject.elasticsearch_host).to eq("ac31ebb90241773157043c34fd26fd46.us-central1.gcp.cloud.es.io:443") - end - it "captures the kibana host" do - expect(subject.kibana_host).to eq("a4c06230e48c8fce7be88a074a3bb3e0.us-central1.gcp.cloud.es.io:443") - end - it "captures the remaining identifiers" do - expect(subject.other_identifiers).to eq(["anotherid", "andanother"]) - end - end - - describe "when given acceptable input (with empty kibana uuid), the accessors:" do - let(:input) { "a-test:ZWNlLmhvbWUubGFuJHRlc3Qk" } # ece.home.lan$test$ - - it '#original has a value' do - expect(subject.original).to eq(input) - end - it '#decoded has a value' do - expect(subject.decoded).to eq("ece.home.lan$test$") - end - it '#label has a value' do - expect(subject.label).to eq("a-test") - end - it '#elasticsearch_host has a value' do - expect(subject.elasticsearch_host).to eq("test.ece.home.lan:443") - end - it '#elasticsearch_scheme has a value' do - expect(subject.elasticsearch_scheme).to eq("https") - end - it '#kibana_host has a value' do - # NOTE: kibana part is not relevant -> this is how python/beats(go) code behaves - expect(subject.kibana_host).to eq(".ece.home.lan:443") - end - it '#kibana_scheme has a value' do - expect(subject.kibana_scheme).to eq("https") - end - it '#to_s has a value of #decoded' do - expect(subject.to_s).to eq(subject.decoded) - end - end - - describe "a lengthy real-world input, the accessors:" do - let(:input) do - "ZWFzdHVzMi5henVyZS5lbGFzdGljLWNsb3VkLmNvbTo5MjQzJDQwYjM0MzExNmNmYTRlYmNiNzZjMTFlZTIzMjlmOTJkJDQzZDA5MjUyNTAyYzQxODlhMzc2ZmQwY2YyY2QwODQ4" - # eastus2.azure.elastic-cloud.com:9243$40b343116cfa4ebcb76c11ee2329f92d$43d09252502c4189a376fd0cf2cd0848 - end - - it '#original has a value' do - expect(subject.original).to eq(input) - end - it '#decoded has a value' do - expect(subject.decoded).to eq("eastus2.azure.elastic-cloud.com:9243$40b343116cfa4ebcb76c11ee2329f92d$43d09252502c4189a376fd0cf2cd0848") - end - it '#label has a value' do - expect(subject.label).to eq("") - end - it '#elasticsearch_host has a value' do - expect(subject.elasticsearch_host).to eq("40b343116cfa4ebcb76c11ee2329f92d.eastus2.azure.elastic-cloud.com:9243") - end - it '#kibana_host has a value' do - expect(subject.kibana_host).to eq("43d09252502c4189a376fd0cf2cd0848.eastus2.azure.elastic-cloud.com:9243") - end - it '#to_s has a value of #decoded' do - expect(subject.to_s).to eq(subject.decoded) - end - end -end diff --git a/logstash-core/src/main/java/org/logstash/util/CloudSettingId.java b/logstash-core/src/main/java/org/logstash/util/CloudSettingId.java new file mode 100644 index 000000000..c51104908 --- /dev/null +++ b/logstash-core/src/main/java/org/logstash/util/CloudSettingId.java @@ -0,0 +1,191 @@ +/* + * 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 org.logstash.util; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Base64; + +/* + * The constructor is expecting a 'cloud.id', a string in 2 variants. + * 1 part example: 'dXMtZWFzdC0xLmF3cy5mb3VuZC5pbyRub3RhcmVhbCRpZGVudGlmaWVy' + * 2 part example: 'foobar:dXMtZWFzdC0xLmF3cy5mb3VuZC5pbyRub3RhcmVhbCRpZGVudGlmaWVy' + * The two part variant has a 'label' prepended with a colon separator. The label is not encoded. + * The 1 part (or second section of the 2 part variant) is base64 encoded. + * The original string before encoding has three segments separated by a dollar sign. + * e.g. 'us-east-1.aws.found.io$notareal$identifier' + * The first segment is the cloud base url, e.g. 'us-east-1.aws.found.io' + * The second segment is the elasticsearch host identifier, e.g. 'notareal' + * The third segment is the kibana host identifier, e.g. 'identifier' + * The 'cloud.id' value decoded into the various fields. + */ +public class CloudSettingId { + + private static class HostAndPort { + static final HostAndPort NO_HOST = new HostAndPort("", null); + private final String host; + private final String port; + + private HostAndPort(String host, String port) { + this.host = host; + this.port = port; + } + + String portOrDefault(String defaultPort) { + return port == null ? defaultPort : port; + } + + static HostAndPort parseHostAndPort(String part, String guidanceMessageWhenHostEqualsUndefined) { + final String[] hostParts = part.split(":"); + String host = hostParts[0]; + if ("undefined".equals(host)) { + throw new IllegalArgumentException(guidanceMessageWhenHostEqualsUndefined); + } + String port = null; + if (hostParts.length > 1) { + port = hostParts[1]; + } + return new HostAndPort(host, port); + } + } + + public static final String DOT_SEPARATOR = "."; + public static final String CLOUD_PORT = "443"; + + private String original; + private String decoded; + private String label; + private String elasticsearchScheme; + private String elasticsearchHost; + private String elasticsearchPort; + private String kibanaScheme; + private String kibanaHost; + private String kibanaPort; + private String[] otherIdentifiers = new String[0]; + + public CloudSettingId(String value) { + if (value == null) { + return; + } + original = value; + final String[] parts = original.split(":"); + label = parts[0]; + String encoded = null; + if (parts.length > 1) { + encoded = parts[1]; + } + if (encoded == null || encoded.isEmpty()) { + try { + decoded = new String(Base64.getUrlDecoder().decode(label), StandardCharsets.UTF_8); + } catch (IllegalArgumentException iaex) { + decoded = ""; + } + label = ""; + } else { + try { + decoded = new String(Base64.getUrlDecoder().decode(encoded), StandardCharsets.UTF_8); + } catch (IllegalArgumentException iaex) { + decoded = ""; + } + } + long separatorCount = decoded.chars().filter(c -> c == '$').count(); + if (separatorCount < 2) { + throw new IllegalArgumentException("Cloud Id, after decoding, is invalid. Format: '$$'. Received: \"" + decoded + "\"."); + } + final String[] segments = decoded.split("\\$"); + if (Arrays.stream(segments).anyMatch(String::isEmpty)) { + throw new IllegalArgumentException("Cloud Id, after decoding, is invalid. Format: '$$'. Received: \"" + decoded + "\"."); + } + String cloudBase = segments[0]; + String cloudHost = DOT_SEPARATOR + cloudBase; + final String[] hostParts = cloudHost.split(":"); + final HostAndPort cloud = new HostAndPort(hostParts[0], hostParts.length > 1 ? hostParts[1] : CLOUD_PORT); + + final HostAndPort elasticsearch = HostAndPort.parseHostAndPort(segments[1], "Cloud Id, after decoding, elasticsearch segment is 'undefined', literally."); + elasticsearchPort = elasticsearch.portOrDefault(cloud.port); + elasticsearchHost = elasticsearch.host + cloud.host + ":" + elasticsearchPort; + elasticsearchScheme = "https"; + + final HostAndPort kibana; + if (segments.length > 2) { + kibana = HostAndPort.parseHostAndPort(segments[2], "Cloud Id, after decoding, the kibana segment is 'undefined', literally. You may need to enable Kibana in the Cloud UI."); + } else { + // non-sense really to have '.my-host:443' but we're mirroring others + kibana = HostAndPort.NO_HOST; + } + kibanaPort = kibana.portOrDefault(cloud.port); + kibanaHost = kibana.host + cloud.host + ":" + kibanaPort; + kibanaScheme = "https"; + + if (segments.length > 3) { + otherIdentifiers = Arrays.copyOfRange(segments, 3, segments.length); + } + } + + public String getOriginal() { + return original; + } + + public String getDecoded() { + return decoded; + } + + public String getLabel() { + return label; + } + + public String getElasticsearchScheme() { + return elasticsearchScheme; + } + + public String getElasticsearchHost() { + return elasticsearchHost; + } + + public String getElasticsearchPort() { + return elasticsearchPort; + } + + public String getKibanaScheme() { + return kibanaScheme; + } + + public String getKibanaHost() { + return kibanaHost; + } + + public String getKibanaPort() { + return kibanaPort; + } + + public String[] getOtherIdentifiers() { + return otherIdentifiers; + } + + @Override + public String toString() { + return decoded; + } + + public static String cloudIdEncode(String... args) { + final String joinedArgs = String.join("$", args); + return Base64.getUrlEncoder().encodeToString(joinedArgs.getBytes()); + } +} diff --git a/logstash-core/src/test/java/org/logstash/util/CloudSettingIdTest.java b/logstash-core/src/test/java/org/logstash/util/CloudSettingIdTest.java new file mode 100644 index 000000000..0525d6bbe --- /dev/null +++ b/logstash-core/src/test/java/org/logstash/util/CloudSettingIdTest.java @@ -0,0 +1,194 @@ +/* + * 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 org.logstash.util; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static org.junit.Assert.*; + +public class CloudSettingIdTest { + + private String input = "foobar:dXMtZWFzdC0xLmF3cy5mb3VuZC5pbyRub3RhcmVhbCRpZGVudGlmaWVy"; + private CloudSettingId sut; + + @Rule + public ExpectedException exceptionRule = ExpectedException.none(); + + @Before + public void setUp() { + sut = new CloudSettingId(input); + } + + // when given unacceptable input + @Test + public void testNullInputDoenstThrowAnException() { + new CloudSettingId(null); + } + + @Test + public void testNullInputMakesAllGettersReturnNull() { + sut = new CloudSettingId(null); + assertNull(sut.getOriginal()); + assertNull(sut.getDecoded()); + assertNull(sut.getLabel()); + assertNull(sut.getElasticsearchHost()); + assertNull(sut.getKibanaHost()); + assertNull(sut.getElasticsearchScheme()); + assertNull(sut.getKibanaScheme()); + } + + @Test + public void testThrowExceptionWhenMalformedValueIsGiven() { + String[] raw = new String[] {"first", "second"}; + String encoded = CloudSettingId.cloudIdEncode(raw); + exceptionRule.expect(IllegalArgumentException.class); + exceptionRule.expectMessage("Cloud Id, after decoding, is invalid. Format: '$$'. Received: \"" + String.join("$", raw) + "\"."); + + new CloudSettingId(encoded); + } + + @Test + public void testThrowExceptionWhenAtLeatOneSegmentIsEmpty() { + String[] raw = new String[] {"first", "", "third"}; + String encoded = CloudSettingId.cloudIdEncode(raw); + exceptionRule.expect(IllegalArgumentException.class); + exceptionRule.expectMessage("Cloud Id, after decoding, is invalid. Format: '$$'. Received: \"" + String.join("$", raw) + "\"."); + + new CloudSettingId(encoded); + } + + @Test + public void testThrowExceptionWhenElasticSegmentSegmentIsUndefined() { + String[] raw = new String[] {"us-east-1.aws.found.io", "undefined", "my-kibana"}; + String encoded = CloudSettingId.cloudIdEncode(raw); + exceptionRule.expect(IllegalArgumentException.class); + exceptionRule.expectMessage("Cloud Id, after decoding, elasticsearch segment is 'undefined', literally."); + + new CloudSettingId(encoded); + } + + @Test + public void testThrowExceptionWhenKibanaSegmentSegmentIsUndefined() { + String[] raw = new String[] {"us-east-1.aws.found.io", "my-elastic-cluster", "undefined"}; + String encoded = CloudSettingId.cloudIdEncode(raw); + exceptionRule.expect(IllegalArgumentException.class); + exceptionRule.expectMessage("Cloud Id, after decoding, the kibana segment is 'undefined', literally. You may need to enable Kibana in the Cloud UI."); + + new CloudSettingId(encoded); + } + + // without a label + @Test + public void testDecodingWithoutLabelSegment() { + sut = new CloudSettingId("dXMtZWFzdC0xLmF3cy5mb3VuZC5pbyRub3RhcmVhbCRpZGVudGlmaWVy"); + + assertEquals("#label is empty", "", sut.getLabel()); + assertEquals("#decode is set", "us-east-1.aws.found.io$notareal$identifier", sut.getDecoded()); + } + + // when given acceptable input, the accessors: + @Test + public void testAccessorsWithAcceptableInput() { + assertEquals("#original has a value", input, sut.getOriginal()); + assertEquals("#decoded has a value", "us-east-1.aws.found.io$notareal$identifier", sut.getDecoded()); + assertEquals("#label has a value", "foobar", sut.getLabel()); + assertEquals("#elasticsearch_host has a value", "notareal.us-east-1.aws.found.io:443", sut.getElasticsearchHost()); + assertEquals("#elasticsearch_scheme has a value", "https", sut.getElasticsearchScheme()); + assertEquals("#kibana_host has a value", "identifier.us-east-1.aws.found.io:443", sut.getKibanaHost()); + assertEquals("#kibana_scheme has a value", "https", sut.getKibanaScheme()); + assertEquals("#to_s has a value of #decoded", sut.toString(), sut.getDecoded()); + } + + @Test + public void testWhenCloudIdContainsPortDescriptionForESAndKibana() { + sut = new CloudSettingId("different-es-kb-port:dXMtY2VudHJhbDEuZ2NwLmNsb3VkLmVzLmlvJGFjMzFlYmI5MDI0MTc3MzE1NzA0M2MzNGZkMjZmZDQ2OjkyNDMkYTRjMDYyMzBlNDhjOGZjZTdiZTg4YTA3NGEzYmIzZTA6OTI0NA=="); + + assertEquals("decodes the elasticsearch port corretly", "ac31ebb90241773157043c34fd26fd46.us-central1.gcp.cloud.es.io:9243", sut.getElasticsearchHost()); + assertEquals("decodes the kibana port corretly", "a4c06230e48c8fce7be88a074a3bb3e0.us-central1.gcp.cloud.es.io:9244", sut.getKibanaHost()); + } + + @Test + public void testWhenCloudIdContainsCloudPort() { + sut = new CloudSettingId("custom-port:dXMtY2VudHJhbDEuZ2NwLmNsb3VkLmVzLmlvOjkyNDMkYWMzMWViYjkwMjQxNzczMTU3MDQzYzM0ZmQyNmZkNDYkYTRjMDYyMzBlNDhjOGZjZTdiZTg4YTA3NGEzYmIzZTA="); + + assertEquals("decodes the elasticsearch port corretly", "ac31ebb90241773157043c34fd26fd46.us-central1.gcp.cloud.es.io:9243", sut.getElasticsearchHost()); + assertEquals("decodes the kibana port corretly", "a4c06230e48c8fce7be88a074a3bb3e0.us-central1.gcp.cloud.es.io:9243", sut.getKibanaHost()); + } + + @Test + public void testWhenCloudIdOnlyDefinesKibanaPort() { + sut = new CloudSettingId("only-kb-set:dXMtY2VudHJhbDEuZ2NwLmNsb3VkLmVzLmlvJGFjMzFlYmI5MDI0MTc3MzE1NzA0M2MzNGZkMjZmZDQ2JGE0YzA2MjMwZTQ4YzhmY2U3YmU4OGEwNzRhM2JiM2UwOjkyNDQ="); + + assertEquals("defaults the elasticsearch port to 443", "ac31ebb90241773157043c34fd26fd46.us-central1.gcp.cloud.es.io:443", sut.getElasticsearchHost()); + assertEquals("decodes the kibana port corretly", "a4c06230e48c8fce7be88a074a3bb3e0.us-central1.gcp.cloud.es.io:9244", sut.getKibanaHost()); + } + + @Test + public void testWhenCloudIdDefinesCloudPortAndKibanaPort() { + sut = new CloudSettingId("host-and-kb-set:dXMtY2VudHJhbDEuZ2NwLmNsb3VkLmVzLmlvOjkyNDMkYWMzMWViYjkwMjQxNzczMTU3MDQzYzM0ZmQyNmZkNDYkYTRjMDYyMzBlNDhjOGZjZTdiZTg4YTA3NGEzYmIzZTA6OTI0NA=="); + + assertEquals("sets the elasticsearch port to cloud port", "ac31ebb90241773157043c34fd26fd46.us-central1.gcp.cloud.es.io:9243", sut.getElasticsearchHost()); + assertEquals("overrides cloud port with the kibana port", "a4c06230e48c8fce7be88a074a3bb3e0.us-central1.gcp.cloud.es.io:9244", sut.getKibanaHost()); + } + + @Test + public void testWhenCloudIdDefinesExtraData() { + sut = new CloudSettingId("extra-items:dXMtY2VudHJhbDEuZ2NwLmNsb3VkLmVzLmlvJGFjMzFlYmI5MDI0MTc3MzE1NzA0M2MzNGZkMjZmZDQ2JGE0YzA2MjMwZTQ4YzhmY2U3YmU4OGEwNzRhM2JiM2UwJGFub3RoZXJpZCRhbmRhbm90aGVy"); + + assertEquals("captures the elasticsearch host", "ac31ebb90241773157043c34fd26fd46.us-central1.gcp.cloud.es.io:443", sut.getElasticsearchHost()); + assertEquals("captures the kibana host", "a4c06230e48c8fce7be88a074a3bb3e0.us-central1.gcp.cloud.es.io:443", sut.getKibanaHost()); + assertArrayEquals("captures the remaining identifiers", new String[] {"anotherid", "andanother"}, sut.getOtherIdentifiers()); + } + + // when given acceptable input (with empty kibana uuid), the accessors: + @Test + public void testGivenAcceptableInputEmptyKibanaUUID() { + input = "a-test:ZWNlLmhvbWUubGFuJHRlc3Qk"; + sut = new CloudSettingId(input); // ece.home.lan$test$ + + assertEquals("#original has a value", input, sut.getOriginal()); + assertEquals("#decoded has a value", "ece.home.lan$test$", sut.getDecoded()); + assertEquals("#label has a value", "a-test", sut.getLabel()); + assertEquals("#elasticsearch_host has a value", "test.ece.home.lan:443", sut.getElasticsearchHost()); + assertEquals("#elasticsearch_scheme has a value", "https", sut.getElasticsearchScheme()); + // NOTE: kibana part is not relevant -> this is how python/beats(go) code behaves + assertEquals("#kibana_host has a value", ".ece.home.lan:443", sut.getKibanaHost()); + assertEquals("#kibana_scheme has a value", "https", sut.getKibanaScheme()); + assertEquals("#toString has a value of #decoded", sut.getDecoded(), sut.toString()); + } + + // a lengthy real-world input, the accessors: + @Test + public void testWithRealWorldInput() { + //eastus2.azure.elastic-cloud.com:9243$40b343116cfa4ebcb76c11ee2329f92d$43d09252502c4189a376fd0cf2cd0848 + input = "ZWFzdHVzMi5henVyZS5lbGFzdGljLWNsb3VkLmNvbTo5MjQzJDQwYjM0MzExNmNmYTRlYmNiNzZjMTFlZTIzMjlmOTJkJDQzZDA5MjUyNTAyYzQxODlhMzc2ZmQwY2YyY2QwODQ4"; + sut = new CloudSettingId(input); + + assertEquals("#original has a value", input, sut.getOriginal()); + assertEquals("#decoded has a value", "eastus2.azure.elastic-cloud.com:9243$40b343116cfa4ebcb76c11ee2329f92d$43d09252502c4189a376fd0cf2cd0848", sut.getDecoded()); + assertEquals("#label has a value", "", sut.getLabel()); + assertEquals("#elasticsearch_host has a value", "40b343116cfa4ebcb76c11ee2329f92d.eastus2.azure.elastic-cloud.com:9243", sut.getElasticsearchHost()); + assertEquals("#kibana_host has a value", "43d09252502c4189a376fd0cf2cd0848.eastus2.azure.elastic-cloud.com:9243", sut.getKibanaHost()); + assertEquals("#toString has a value of #decoded", sut.getDecoded(), sut.toString()); + } +}