diff options
9 files changed, 146 insertions, 114 deletions
diff --git a/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentSpec.java b/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentSpec.java index 1a2335c82db..64e986abf9b 100644 --- a/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentSpec.java +++ b/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentSpec.java @@ -3,6 +3,8 @@ package com.yahoo.config.application.api; import com.google.common.collect.ImmutableList; import com.yahoo.config.application.api.xml.DeploymentSpecXmlReader; +import com.yahoo.config.provision.AthenzDomain; +import com.yahoo.config.provision.AthenzService; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.RegionName; @@ -48,8 +50,8 @@ public class DeploymentSpec { private final List<ChangeBlocker> changeBlockers; private final List<Step> steps; private final String xmlForm; - private final Optional<String> athenzDomain; - private final Optional<String> athenzService; + private final Optional<AthenzDomain> athenzDomain; + private final Optional<AthenzService> athenzService; public DeploymentSpec(Optional<String> globalServiceId, UpgradePolicy upgradePolicy, List<ChangeBlocker> changeBlockers, List<Step> steps) { @@ -58,7 +60,7 @@ public class DeploymentSpec { public DeploymentSpec(Optional<String> globalServiceId, UpgradePolicy upgradePolicy, List<ChangeBlocker> changeBlockers, List<Step> steps, String xmlForm, - Optional<String> athenzDomain, Optional<String> athenzService) { + Optional<AthenzDomain> athenzDomain, Optional<AthenzService> athenzService) { validateTotalDelay(steps); this.globalServiceId = globalServiceId; this.upgradePolicy = upgradePolicy; @@ -103,7 +105,7 @@ public class DeploymentSpec { throw new IllegalArgumentException("Athenz service configured for zone: " + zone + ", but Athenz domain is not configured"); } } - // if athenz domain is configured, athenz service must be set implicitly or directly on all zones. + // if athenz domain is not set, athenz service must be set implicitly or directly on all zones. } else if(! athenzService.isPresent()) { for (DeclaredZone zone : zones()) { if(! zone.athenzService().isPresent()) { @@ -244,17 +246,18 @@ public class DeploymentSpec { } /** Returns the athenz domain if configured */ - public Optional<String> athenzDomain() { + public Optional<AthenzDomain> athenzDomain() { return athenzDomain; } /** Returns the athenz service for environment/region if configured */ - public Optional<String> athenzService(Environment environment, RegionName region) { - return zones().stream() + public Optional<AthenzService> athenzService(Environment environment, RegionName region) { + AthenzService athenzService = zones().stream() .filter(zone -> zone.deploysTo(environment, Optional.of(region))) .findFirst() - .map(DeclaredZone::athenzService) - .orElse(athenzService); + .flatMap(DeclaredZone::athenzService) + .orElse(this.athenzService.orElse(null)); + return Optional.ofNullable(athenzService); } /** This may be invoked by a continuous build */ @@ -321,7 +324,7 @@ public class DeploymentSpec { private final boolean active; - private Optional<String> athenzService; + private Optional<AthenzService> athenzService; public DeclaredZone(Environment environment) { this(environment, Optional.empty(), false); @@ -331,7 +334,7 @@ public class DeploymentSpec { this(environment, region, active, Optional.empty()); } - public DeclaredZone(Environment environment, Optional<RegionName> region, boolean active, Optional<String> athenzService) { + public DeclaredZone(Environment environment, Optional<RegionName> region, boolean active, Optional<AthenzService> athenzService) { if (environment != Environment.prod && region.isPresent()) throw new IllegalArgumentException("Non-prod environments cannot specify a region"); if (environment == Environment.prod && ! region.isPresent()) @@ -350,7 +353,7 @@ public class DeploymentSpec { /** Returns whether this zone should receive production traffic */ public boolean active() { return active; } - public Optional<String> athenzService() { return athenzService; } + public Optional<AthenzService> athenzService() { return athenzService; } @Override public List<DeclaredZone> zones() { return Collections.singletonList(this); } diff --git a/config-model-api/src/main/java/com/yahoo/config/application/api/xml/DeploymentSpecXmlReader.java b/config-model-api/src/main/java/com/yahoo/config/application/api/xml/DeploymentSpecXmlReader.java index 1a970e53713..8bc4e0026a6 100644 --- a/config-model-api/src/main/java/com/yahoo/config/application/api/xml/DeploymentSpecXmlReader.java +++ b/config-model-api/src/main/java/com/yahoo/config/application/api/xml/DeploymentSpecXmlReader.java @@ -7,6 +7,8 @@ import com.yahoo.config.application.api.DeploymentSpec.Delay; import com.yahoo.config.application.api.DeploymentSpec.ParallelZones; import com.yahoo.config.application.api.DeploymentSpec.Step; import com.yahoo.config.application.api.TimeWindow; +import com.yahoo.config.provision.AthenzDomain; +import com.yahoo.config.provision.AthenzService; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.RegionName; import com.yahoo.io.IOUtils; @@ -72,7 +74,7 @@ public class DeploymentSpecXmlReader { if (environment == Environment.prod) { for (Element stepTag : XML.getChildren(environmentTag)) { - Optional<String> athenzService = stringAttribute("athenz-service", environmentTag); + Optional<AthenzService> athenzService = stringAttribute("athenz-service", environmentTag).map(AthenzService::from); if (stepTag.getTagName().equals("delay")) { steps.add(new Delay(Duration.ofSeconds(longAttribute("hours", stepTag) * 60 * 60 + longAttribute("minutes", stepTag) * 60 + @@ -97,8 +99,8 @@ public class DeploymentSpecXmlReader { throw new IllegalArgumentException("Attribute 'global-service-id' is only valid on 'prod' tag."); } - Optional<String> athenzDomain = stringAttribute("athenz-domain", root); - Optional<String> athenzService = stringAttribute("athenz-service", root); + Optional<AthenzDomain> athenzDomain = stringAttribute("athenz-domain", root).map(AthenzDomain::from); + Optional<AthenzService> athenzService = stringAttribute("athenz-service", root).map(AthenzService::from); return new DeploymentSpec(globalServiceId, readUpgradePolicy(root), readChangeBlockers(root), steps, xmlForm, athenzDomain, athenzService); } @@ -146,7 +148,7 @@ public class DeploymentSpecXmlReader { return tagName.equals(testTag) || tagName.equals(stagingTag) || tagName.equals(prodTag); } - private DeclaredZone readDeclaredZone(Environment environment, Optional<String> athenzService, Element regionTag) { + private DeclaredZone readDeclaredZone(Environment environment, Optional<AthenzService> athenzService, Element regionTag) { return new DeclaredZone(environment, Optional.of(RegionName.from(XML.getValue(regionTag).trim())), readActive(regionTag), athenzService); } diff --git a/config-model-api/src/test/java/com/yahoo/config/application/api/DeploymentSpecTest.java b/config-model-api/src/test/java/com/yahoo/config/application/api/DeploymentSpecTest.java index 5dcb3bc1ebe..99243125f9c 100644 --- a/config-model-api/src/test/java/com/yahoo/config/application/api/DeploymentSpecTest.java +++ b/config-model-api/src/test/java/com/yahoo/config/application/api/DeploymentSpecTest.java @@ -368,7 +368,7 @@ public class DeploymentSpecTest { } @Test - public void deploymentSpecWithAthenzIdentity() { + public void athenz_config_is_read_from_deployment() { StringReader r = new StringReader( "<deployment athenz-domain='domain' athenz-service='service'>\n" + " <prod>\n" + @@ -382,7 +382,7 @@ public class DeploymentSpecTest { } @Test - public void deploymentSpecUsesServiceFromEnvironment() { + public void athenz_service_is_overridden_from_environment() { StringReader r = new StringReader( "<deployment athenz-domain='domain' athenz-service='service'>\n" + " <test/>\n" + @@ -397,7 +397,7 @@ public class DeploymentSpecTest { } @Test(expected = IllegalArgumentException.class) - public void athenzDomainMissingService() { + public void it_fails_when_athenz_service_is_not_defined() { StringReader r = new StringReader( "<deployment athenz-domain='domain'>\n" + " <prod>\n" + @@ -409,7 +409,7 @@ public class DeploymentSpecTest { } @Test(expected = IllegalArgumentException.class) - public void athenzDomainMissingDomain() { + public void it_fails_when_athenz_service_is_configured_but_not_athenz_domain() { StringReader r = new StringReader( "<deployment>\n" + " <prod athenz-service='service'>\n" + diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/ContainerCluster.java b/config-model/src/main/java/com/yahoo/vespa/model/container/ContainerCluster.java index 1e9dc569ff5..4383e55e45d 100755 --- a/config-model/src/main/java/com/yahoo/vespa/model/container/ContainerCluster.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/container/ContainerCluster.java @@ -9,15 +9,10 @@ import com.yahoo.component.ComponentSpecification; import com.yahoo.config.FileReference; import com.yahoo.config.application.api.ApplicationMetaData; import com.yahoo.config.application.api.ComponentInfo; -import com.yahoo.config.application.api.DeploymentSpec; import com.yahoo.config.docproc.DocprocConfig; import com.yahoo.config.docproc.SchemamappingConfig; import com.yahoo.config.model.ApplicationConfigProducerRoot; -import com.yahoo.config.model.api.ConfigServerSpec; import com.yahoo.config.model.producer.AbstractConfigProducer; -import com.yahoo.config.model.producer.AbstractConfigProducerRoot; -import com.yahoo.config.provision.HostName; -import com.yahoo.config.provision.Rotation; import com.yahoo.config.provision.Zone; import com.yahoo.container.BundlesConfig; import com.yahoo.container.ComponentsConfig; @@ -87,7 +82,6 @@ import com.yahoo.vespaclient.config.FeederConfig; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; -import java.io.Reader; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; @@ -234,7 +228,6 @@ public final class ContainerCluster addSimpleComponent("com.yahoo.container.core.slobrok.SlobrokConfigurator"); addSimpleComponent("com.yahoo.container.handler.VipStatus"); addJaxProviders(); - addIdentity(); } public void setZone(Zone zone) { @@ -307,36 +300,6 @@ public final class ContainerCluster addSimpleComponent(XPathFactoryProvider.class); } - public void addIdentity() { - getDeploymentSpec().ifPresent(deploymentSpec -> { - deploymentSpec.athenzDomain().ifPresent(domain -> { - - String service = deploymentSpec.athenzService(zone.environment(), zone.region()) - .orElseThrow(() -> new RuntimeException("Missing Athenz service configuration")); - - Identity identity = new Identity(domain.trim(), service.trim(), getLoadBalancerName()); - addComponent(identity); - - getContainers().forEach(container -> { - container.setProp("identity.domain", domain); - container.setProp("identity.service", service); - }); - }); - }); - } - - private HostName getLoadBalancerName() { - // Set lbaddress, or use first hostname if not specified. - // TODO: Remove the orElseGet part when this is set up in all zones - return Optional.ofNullable(getRoot().getDeployState().getProperties().loadBalancerName()) - .orElseGet( - () -> HostName.from(getRoot().getDeployState().getProperties().configServerSpecs().stream() - .findFirst() - .map(ConfigServerSpec::getHostName) - .orElse("unknown") // Currently unable to test this, hence the unknown - )); - } - public final void addComponent(Component<?, ?> component) { if (clusterVerifier.acceptComponent(component)) { componentGroup.addComponent(component); @@ -415,8 +378,6 @@ public final class ContainerCluster container.setClusterName(name); container.setProp("clustername", name) .setProp("index", this.containers.size()); - setRotations(container, getRotations(), getGlobalServiceId(), name); - container.setProp("activeRotation", Boolean.toString(getActiveRotation())); containers.add(container); } @@ -426,55 +387,6 @@ public final class ContainerCluster } } - private Optional<String> getGlobalServiceId() { - Optional<DeploymentSpec> deploymentSpec = getDeploymentSpec(); - if (deploymentSpec.isPresent()) return deploymentSpec.get().globalServiceId(); - return Optional.empty(); - } - - private Set<Rotation> getRotations() { - return Optional.ofNullable(getRoot()) - .map(root -> root.getDeployState().getRotations()) - .orElse(Collections.emptySet()); - } - - private boolean getActiveRotation() { - return Optional.ofNullable(getRoot()) - .map(root -> root.getDeployState().getProperties().zone()) - .map(this::zoneHasActiveRotation) - .orElse(false); - } - - private boolean zoneHasActiveRotation(Zone zone) { - Optional<DeploymentSpec> spec = getDeploymentSpec(); - if (!spec.isPresent()) { - return false; - } - return spec.get().zones().stream() - .anyMatch(declaredZone -> declaredZone.deploysTo(zone.environment(), Optional.of(zone.region())) && - declaredZone.active()); - } - - private Optional<DeploymentSpec> getDeploymentSpec() { - Optional<DeploymentSpec> deploymentSpec = Optional.empty(); - AbstractConfigProducerRoot root = getRoot(); - if (root != null) { - final Optional<Reader> deployment = root.getDeployState().getApplicationPackage().getDeployment(); - if (deployment.isPresent()) { - deploymentSpec = Optional.of(DeploymentSpec.fromXml(deployment.get())); - } - } - return deploymentSpec; - } - - private void setRotations(Container container, Set<Rotation> rotations, Optional<String> globalServiceId, String containerClusterName) { - if ( ! rotations.isEmpty() && globalServiceId.isPresent()) { - if (containerClusterName.equals(globalServiceId.get())) { - container.setProp("rotations", rotations.stream().map(Rotation::getId).collect(Collectors.joining(","))); - } - } - } - public void setProcessingChains(ProcessingChains processingChains, String... serverBindings) { if (this.processingChains != null) throw new IllegalStateException("ProcessingChains should only be set once."); diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/Identity.java b/config-model/src/main/java/com/yahoo/vespa/model/container/Identity.java index bc7a6e20361..bf5a7757149 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/container/Identity.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/container/Identity.java @@ -1,6 +1,8 @@ // Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.model.container; +import com.yahoo.config.provision.AthenzDomain; +import com.yahoo.config.provision.AthenzService; import com.yahoo.config.provision.HostName; import com.yahoo.container.core.identity.IdentityConfig; import com.yahoo.container.jdisc.athenz.impl.AthenzIdentityProviderImpl; @@ -12,11 +14,11 @@ import com.yahoo.vespa.model.container.component.SimpleComponent; public class Identity extends SimpleComponent implements IdentityConfig.Producer { public static final String CLASS = AthenzIdentityProviderImpl.class.getName(); - private final String domain; - private final String service; + private final AthenzDomain domain; + private final AthenzService service; private final HostName loadBalancerName; - public Identity(String domain, String service, HostName loadBalancerName) { + public Identity(AthenzDomain domain, AthenzService service, HostName loadBalancerName) { super(CLASS); this.domain = domain; this.service = service; @@ -25,8 +27,8 @@ public class Identity extends SimpleComponent implements IdentityConfig.Producer @Override public void getConfig(IdentityConfig.Builder builder) { - builder.domain(domain); - builder.service(service); + builder.domain(domain.value()); + builder.service(service.value()); // Current interpretation of loadbalancer address is: hostname. // Config should be renamed or send the uri builder.loadBalancerAddress(loadBalancerName.value()); diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java index 121039a248d..4dba9f923a8 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java @@ -6,16 +6,22 @@ import com.yahoo.component.Version; import com.yahoo.config.application.Xml; import com.yahoo.config.application.api.ApplicationPackage; import com.yahoo.config.application.api.DeployLogger; +import com.yahoo.config.application.api.DeploymentSpec; import com.yahoo.config.model.ConfigModelContext; +import com.yahoo.config.model.api.ConfigServerSpec; import com.yahoo.config.model.application.provider.IncludeDirs; import com.yahoo.config.model.builder.xml.ConfigModelBuilder; import com.yahoo.config.model.builder.xml.ConfigModelId; import com.yahoo.config.model.producer.AbstractConfigProducer; +import com.yahoo.config.provision.AthenzService; import com.yahoo.config.provision.Capacity; import com.yahoo.config.provision.ClusterMembership; import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.Environment; +import com.yahoo.config.provision.HostName; import com.yahoo.config.provision.NodeType; +import com.yahoo.config.provision.Rotation; +import com.yahoo.config.provision.Zone; import com.yahoo.container.jdisc.config.MetricDefaultsConfig; import com.yahoo.search.rendering.RendererRegistry; import com.yahoo.text.XML; @@ -37,6 +43,7 @@ import com.yahoo.vespa.model.clients.ContainerDocumentApi; import com.yahoo.vespa.model.container.Container; import com.yahoo.vespa.model.container.ContainerCluster; import com.yahoo.vespa.model.container.ContainerModel; +import com.yahoo.vespa.model.container.Identity; import com.yahoo.vespa.model.container.component.Component; import com.yahoo.vespa.model.container.component.FileStatusHandlerComponent; import com.yahoo.vespa.model.container.component.chain.ProcessingHandler; @@ -61,6 +68,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.function.Consumer; import java.util.logging.Logger; import java.util.stream.Collectors; @@ -85,6 +93,7 @@ public class ContainerModelBuilder extends ConfigModelBuilder<ContainerModel> { private final boolean standaloneBuilder; private final Networking networking; protected DeployLogger log; + private Optional<DeploymentSpec> deploymentSpec; public static final List<ConfigModelId> configModelIds = ImmutableList.of(ConfigModelId.fromName("container"), ConfigModelId.fromName("jdisc")); @@ -108,6 +117,7 @@ public class ContainerModelBuilder extends ConfigModelBuilder<ContainerModel> { @Override public void doBuild(ContainerModel model, Element spec, ConfigModelContext modelContext) { app = modelContext.getApplicationPackage(); + deploymentSpec = app.getDeployment().map(DeploymentSpec::fromXml); checkVersion(spec); this.log = modelContext.getDeployLogger(); @@ -162,9 +172,43 @@ public class ContainerModelBuilder extends ConfigModelBuilder<ContainerModel> { addServerProviders(spec, cluster); addLegacyFilters(spec, cluster); // TODO: Remove for Vespa 7 + // Athenz copper argos + // NOTE: Must be done after addNodes() + addIdentity(cluster, + context.getDeployState().getProperties().configServerSpecs(), + context.getDeployState().getProperties().loadBalancerName(), + context.getDeployState().zone()); + + addRotationInfo(cluster, context.getDeployState().zone(), context.getDeployState().getRotations()); + //TODO: overview handler, see DomQrserverClusterBuilder } + private void addRotationInfo(ContainerCluster cluster, Zone zone, Set<Rotation> rotations) { + Optional<String> globalServiceId = deploymentSpec.flatMap(DeploymentSpec::globalServiceId); + cluster.getContainers().forEach(container -> { + setRotations(container, rotations, globalServiceId, cluster.getName()); + container.setProp("activeRotation", Boolean.toString(zoneHasActiveRotation(zone))); + }); + } + + private boolean zoneHasActiveRotation(Zone zone) { + return deploymentSpec.map(DeploymentSpec::zones) + .map(List::stream) + .map(x -> x.anyMatch(declaredZone -> declaredZone.deploysTo(zone.environment(), Optional.of(zone.region())) && + declaredZone.active())) + .orElse(false); + } + + private void setRotations(Container container, Set<Rotation> rotations, Optional<String> globalServiceId, String containerClusterName) { + + if ( ! rotations.isEmpty() && globalServiceId.isPresent()) { + if (containerClusterName.equals(globalServiceId.get())) { + container.setProp("rotations", rotations.stream().map(Rotation::getId).collect(Collectors.joining(","))); + } + } + } + private void addRoutingAliases(ContainerCluster cluster, Element spec, Environment environment) { if (environment != Environment.prod) return; @@ -688,6 +732,35 @@ public class ContainerModelBuilder extends ConfigModelBuilder<ContainerModel> { } } + private void addIdentity(ContainerCluster cluster, List<ConfigServerSpec> configServerSpecs, HostName loadBalancerName, Zone zone) { + deploymentSpec.ifPresent(spec -> { + spec.athenzDomain().ifPresent(domain -> { + AthenzService service = spec.athenzService(zone.environment(), zone.region()) + .orElseThrow(() -> new RuntimeException("Missing Athenz service configuration")); + Identity identity = new Identity(domain, service, getLoadBalancerName(loadBalancerName, configServerSpecs)); + cluster.addComponent(identity); + + cluster.getContainers().forEach(container -> { + container.setProp("identity.domain", domain.value()); + container.setProp("identity.service", service.value()); + }); + }); + }); + } + + private HostName getLoadBalancerName(HostName loadbalancerName, List<ConfigServerSpec> configServerSpecs) { + // Set lbaddress, or use first hostname if not specified. + // TODO: Remove this method and use the loadbalancerName directly + return Optional.ofNullable(loadbalancerName) + .orElseGet( + () -> HostName.from(configServerSpecs.stream() + .findFirst() + .map(ConfigServerSpec::getHostName) + .orElse("unknown") // Currently unable to test this, hence the unknown + )); + } + + /** * Disallow renderers named "DefaultRenderer" or "JsonRenderer" */ diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/xml/IdentityBuilderTest.java b/config-model/src/test/java/com/yahoo/vespa/model/container/xml/IdentityBuilderTest.java index c8a245e68b0..def23be1474 100644 --- a/config-model/src/test/java/com/yahoo/vespa/model/container/xml/IdentityBuilderTest.java +++ b/config-model/src/test/java/com/yahoo/vespa/model/container/xml/IdentityBuilderTest.java @@ -23,7 +23,7 @@ public class IdentityBuilderTest extends ContainerModelBuilderTestBase { @Test public void identity_config_produced_from_deployment_spec() throws IOException, SAXException { Element clusterElem = DomBuilderTest.parse( - "<jdisc id='default' version='1.0'/>"); + "<jdisc id='default' version='1.0'><search /></jdisc>"); String deploymentXml = "<deployment version='1.0' athenz-domain='domain' athenz-service='service'>\n" + " <test/>\n" + " <prod>\n" + diff --git a/config-provisioning/src/main/java/com/yahoo/config/provision/AthenzDomain.java b/config-provisioning/src/main/java/com/yahoo/config/provision/AthenzDomain.java new file mode 100644 index 00000000000..a7367aaac17 --- /dev/null +++ b/config-provisioning/src/main/java/com/yahoo/config/provision/AthenzDomain.java @@ -0,0 +1,20 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.config.provision; + +/** + * @author mortent + */ +public class AthenzDomain { + + private final String name; + + private AthenzDomain(String name) { + this.name = name; + } + + public static AthenzDomain from(String value) { + return new AthenzDomain(value); + } + + public String value() { return name; } +} diff --git a/config-provisioning/src/main/java/com/yahoo/config/provision/AthenzService.java b/config-provisioning/src/main/java/com/yahoo/config/provision/AthenzService.java new file mode 100644 index 00000000000..312145ca36d --- /dev/null +++ b/config-provisioning/src/main/java/com/yahoo/config/provision/AthenzService.java @@ -0,0 +1,20 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.config.provision; + +/** + * @author mortent + */ +public class AthenzService { + + private final String name; + + private AthenzService(String name) { + this.name = name; + } + + public String value() { return name; } + + public static AthenzService from(String value) { + return new AthenzService(value); + } +} |