Search in sources :

Example 1 with ServiceEndpoint

use of com.spotify.helios.common.descriptors.ServiceEndpoint in project helios by spotify.

the class JobServiceRegistrationTest method test.

@Test
public void test() throws Exception {
    startDefaultMaster();
    final HeliosClient client = defaultClient();
    startDefaultAgent(testHost(), "--service-registry=" + registryAddress);
    awaitHostStatus(client, testHost(), UP, LONG_WAIT_SECONDS, SECONDS);
    final ImmutableMap<String, PortMapping> portMapping = ImmutableMap.of("foo_port", PortMapping.of(4711, externalPort), "bar_port", PortMapping.of(4712));
    final ImmutableMap<ServiceEndpoint, ServicePorts> registration = ImmutableMap.of(ServiceEndpoint.of("foo_service", "foo_proto"), ServicePorts.of("foo_port"), ServiceEndpoint.of("bar_service", "bar_proto"), ServicePorts.of("bar_port"));
    final JobId jobId = createJob(testJobName, testJobVersion, BUSYBOX, IDLE_COMMAND, EMPTY_ENV, portMapping, registration);
    deployJob(jobId, testHost());
    awaitJobState(client, testHost(), jobId, RUNNING, LONG_WAIT_SECONDS, SECONDS);
    verify(registrar, timeout((int) SECONDS.toMillis(LONG_WAIT_SECONDS))).register(registrationCaptor.capture());
    final ServiceRegistration serviceRegistration = registrationCaptor.getValue();
    final Map<String, Endpoint> registered = Maps.newHashMap();
    for (final Endpoint endpoint : serviceRegistration.getEndpoints()) {
        registered.put(endpoint.getName(), endpoint);
    }
    assertEquals("wrong service", "foo_service", registered.get("foo_service").getName());
    assertEquals("wrong protocol", "foo_proto", registered.get("foo_service").getProtocol());
    assertEquals("wrong port", externalPort, registered.get("foo_service").getPort());
    assertEquals("wrong service", "bar_service", registered.get("bar_service").getName());
    assertEquals("wrong protocol", "bar_proto", registered.get("bar_service").getProtocol());
    assertNotEquals("wrong port", externalPort, registered.get("bar_service").getPort());
}
Also used : ServiceEndpoint(com.spotify.helios.common.descriptors.ServiceEndpoint) Endpoint(com.spotify.helios.serviceregistration.ServiceRegistration.Endpoint) ServicePorts(com.spotify.helios.common.descriptors.ServicePorts) PortMapping(com.spotify.helios.common.descriptors.PortMapping) HeliosClient(com.spotify.helios.client.HeliosClient) ServiceEndpoint(com.spotify.helios.common.descriptors.ServiceEndpoint) JobId(com.spotify.helios.common.descriptors.JobId) ServiceRegistration(com.spotify.helios.serviceregistration.ServiceRegistration) Test(org.junit.Test)

Example 2 with ServiceEndpoint

use of com.spotify.helios.common.descriptors.ServiceEndpoint in project helios by spotify.

the class CliJobCreationTest method testMergeFileAndCliArgs.

@Test
public void testMergeFileAndCliArgs() throws Exception {
    final Map<String, PortMapping> ports = ImmutableMap.of("foo", PortMapping.of(4711), "bar", PortMapping.of(5000, externalPort));
    final Map<ServiceEndpoint, ServicePorts> registration = ImmutableMap.of(ServiceEndpoint.of("foo-service", "tcp"), ServicePorts.of("foo"), ServiceEndpoint.of("bar-service", "http"), ServicePorts.of("bar"));
    final String registrationDomain = "my-domain";
    final Map<String, String> env = ImmutableMap.of("BAD", "f00d");
    final Map<String, String> volumes = Maps.newHashMap();
    volumes.put("/etc/spotify/secret-keys.yaml:ro", "/etc/spotify/secret-keys.yaml");
    // Create a new job, serialize it to JSON
    final Job.Builder builder = Job.newBuilder().setCommand(Lists.newArrayList("server", "foo-service.yaml")).setEnv(env).setPorts(ports).setRegistration(registration).setRegistrationDomain(registrationDomain).setVolumes(volumes).setCreatingUser(TEST_USER).setToken("foo-token").setNetworkMode("host").setSecurityOpt(ImmutableList.of("label:user:dxia", "apparmor:foo"));
    final Job job = builder.build();
    final String jobConfigJsonString = job.toJsonString();
    // Create temporary job config file
    final File file = temporaryFolder.newFile();
    final String absolutePath = file.getAbsolutePath();
    // Write JSON config to temp file
    try (final FileOutputStream outFile = new FileOutputStream(file)) {
        outFile.write(Charsets.UTF_8.encode(jobConfigJsonString).array());
        // Create job and specify the temp file.
        cli("create", "--file", absolutePath, testJobNameAndVersion, BUSYBOX);
        // Inspect the job.
        final String actualJobConfigJson = cli("inspect", testJobNameAndVersion, "--json");
        // Compare to make sure the created job has the expected configuration,
        // i.e. the configuration resulting from a merge of the JSON file and CLI args.
        final Job actualJob = Json.read(actualJobConfigJson, Job.class);
        final Job.Builder actualJobBuilder = actualJob.toBuilder();
        builder.setName(testJobName).setVersion(testJobVersion).setImage(BUSYBOX);
        assertJobEquals(builder.build(), actualJobBuilder.build());
    }
}
Also used : ServicePorts(com.spotify.helios.common.descriptors.ServicePorts) FileOutputStream(java.io.FileOutputStream) PortMapping(com.spotify.helios.common.descriptors.PortMapping) Job(com.spotify.helios.common.descriptors.Job) File(java.io.File) ServiceEndpoint(com.spotify.helios.common.descriptors.ServiceEndpoint) Test(org.junit.Test)

Example 3 with ServiceEndpoint

use of com.spotify.helios.common.descriptors.ServiceEndpoint in project helios by spotify.

the class JobCreateCommand method run.

@Override
int run(final Namespace options, final HeliosClient client, final PrintStream out, final boolean json, final BufferedReader stdin) throws ExecutionException, InterruptedException, IOException {
    final boolean quiet = options.getBoolean(quietArg.getDest());
    final Job.Builder builder;
    final String id = options.getString(idArg.getDest());
    final String imageIdentifier = options.getString(imageArg.getDest());
    // Read job configuration from file
    // TODO (dano): look for e.g. Heliosfile in cwd by default?
    final String templateJobId = options.getString(templateArg.getDest());
    final File file = options.get(fileArg.getDest());
    if (file != null && templateJobId != null) {
        throw new IllegalArgumentException("Please use only one of -t/--template and -f/--file");
    }
    if (file != null) {
        if (!file.exists() || !file.isFile() || !file.canRead()) {
            throw new IllegalArgumentException("Cannot read file " + file);
        }
        final byte[] bytes = Files.readAllBytes(file.toPath());
        final String config = new String(bytes, UTF_8);
        final Job job = Json.read(config, Job.class);
        builder = job.toBuilder();
    } else if (templateJobId != null) {
        final Map<JobId, Job> jobs = client.jobs(templateJobId).get();
        if (jobs.size() == 0) {
            if (!json) {
                out.printf("Unknown job: %s%n", templateJobId);
            } else {
                final CreateJobResponse createJobResponse = new CreateJobResponse(CreateJobResponse.Status.UNKNOWN_JOB, null, null);
                out.print(createJobResponse.toJsonString());
            }
            return 1;
        } else if (jobs.size() > 1) {
            if (!json) {
                out.printf("Ambiguous job reference: %s%n", templateJobId);
            } else {
                final CreateJobResponse createJobResponse = new CreateJobResponse(CreateJobResponse.Status.AMBIGUOUS_JOB_REFERENCE, null, null);
                out.print(createJobResponse.toJsonString());
            }
            return 1;
        }
        final Job template = Iterables.getOnlyElement(jobs.values());
        builder = template.toBuilder();
        if (id == null) {
            throw new IllegalArgumentException("Please specify new job name and version");
        }
    } else {
        if (id == null || imageIdentifier == null) {
            throw new IllegalArgumentException("Please specify a file, or a template, or a job name, version and container image");
        }
        builder = Job.newBuilder();
    }
    if (id != null) {
        final String[] parts = id.split(":");
        switch(parts.length) {
            case 3:
                builder.setHash(parts[2]);
            // fall through
            case 2:
                builder.setVersion(parts[1]);
            // fall through
            case 1:
                builder.setName(parts[0]);
                break;
            default:
                throw new IllegalArgumentException("Invalid Job id: " + id);
        }
    }
    if (imageIdentifier != null) {
        builder.setImage(imageIdentifier);
    }
    final String hostname = options.getString(hostnameArg.getDest());
    if (!isNullOrEmpty(hostname)) {
        builder.setHostname(hostname);
    }
    final List<String> command = options.getList(argsArg.getDest());
    if (command != null && !command.isEmpty()) {
        builder.setCommand(command);
    }
    final List<String> envList = options.getList(envArg.getDest());
    // TODO (mbrown): does this mean that env config is only added when there is a CLI flag too?
    if (!envList.isEmpty()) {
        final Map<String, String> env = Maps.newHashMap();
        // Add environmental variables from helios job configuration file
        env.putAll(builder.getEnv());
        // Add environmental variables passed in via CLI
        // Overwrite any redundant keys to make CLI args take precedence
        env.putAll(parseListOfPairs(envList, "environment variable"));
        builder.setEnv(env);
    }
    final Map<String, String> metadata = Maps.newHashMap();
    metadata.putAll(defaultMetadata());
    final List<String> metadataList = options.getList(metadataArg.getDest());
    if (!metadataList.isEmpty()) {
        // TODO (mbrown): values from job conf file (which maybe involves dereferencing env vars?)
        metadata.putAll(parseListOfPairs(metadataList, "metadata"));
    }
    builder.setMetadata(metadata);
    // Parse port mappings
    final List<String> portSpecs = options.getList(portArg.getDest());
    final Map<String, PortMapping> explicitPorts = PortMappingParser.parsePortMappings(portSpecs);
    // Merge port mappings
    final Map<String, PortMapping> ports = Maps.newHashMap();
    ports.putAll(builder.getPorts());
    ports.putAll(explicitPorts);
    builder.setPorts(ports);
    // Parse service registrations
    final Map<ServiceEndpoint, ServicePorts> explicitRegistration = Maps.newHashMap();
    final Pattern registrationPattern = compile("(?<srv>[a-zA-Z][_\\-\\w]+)(?:/(?<prot>\\w+))?(?:=(?<port>[_\\-\\w]+))?");
    final List<String> registrationSpecs = options.getList(registrationArg.getDest());
    for (final String spec : registrationSpecs) {
        final Matcher matcher = registrationPattern.matcher(spec);
        if (!matcher.matches()) {
            throw new IllegalArgumentException("Bad registration: " + spec);
        }
        final String service = matcher.group("srv");
        final String proto = fromNullable(matcher.group("prot")).or(HTTP);
        final String optionalPort = matcher.group("port");
        final String port;
        if (ports.size() == 0) {
            throw new IllegalArgumentException("Need port mappings for service registration.");
        }
        if (optionalPort == null) {
            if (ports.size() != 1) {
                throw new IllegalArgumentException("Need exactly one port mapping for implicit service registration");
            }
            port = Iterables.getLast(ports.keySet());
        } else {
            port = optionalPort;
        }
        explicitRegistration.put(ServiceEndpoint.of(service, proto), ServicePorts.of(port));
    }
    final String registrationDomain = options.getString(registrationDomainArg.getDest());
    if (!isNullOrEmpty(registrationDomain)) {
        builder.setRegistrationDomain(registrationDomain);
    }
    // Merge service registrations
    final Map<ServiceEndpoint, ServicePorts> registration = Maps.newHashMap();
    registration.putAll(builder.getRegistration());
    registration.putAll(explicitRegistration);
    builder.setRegistration(registration);
    // Get grace period interval
    final Integer gracePeriod = options.getInt(gracePeriodArg.getDest());
    if (gracePeriod != null) {
        builder.setGracePeriod(gracePeriod);
    }
    // Parse volumes
    final List<String> volumeSpecs = options.getList(volumeArg.getDest());
    for (final String spec : volumeSpecs) {
        final String[] parts = spec.split(":", 2);
        switch(parts.length) {
            // Data volume
            case 1:
                builder.addVolume(parts[0]);
                break;
            // Bind mount
            case 2:
                final String path = parts[1];
                final String source = parts[0];
                builder.addVolume(path, source);
                break;
            default:
                throw new IllegalArgumentException("Invalid volume: " + spec);
        }
    }
    // Parse expires timestamp
    final String expires = options.getString(expiresArg.getDest());
    if (expires != null) {
        // Use DateTime to parse the ISO-8601 string
        builder.setExpires(new DateTime(expires).toDate());
    }
    // Parse health check
    final String execString = options.getString(healthCheckExecArg.getDest());
    final List<String> execHealthCheck = (execString == null) ? null : Arrays.asList(execString.split(" "));
    final String httpHealthCheck = options.getString(healthCheckHttpArg.getDest());
    final String tcpHealthCheck = options.getString(healthCheckTcpArg.getDest());
    int numberOfHealthChecks = 0;
    for (final String c : asList(httpHealthCheck, tcpHealthCheck)) {
        if (!isNullOrEmpty(c)) {
            numberOfHealthChecks++;
        }
    }
    if (execHealthCheck != null && !execHealthCheck.isEmpty()) {
        numberOfHealthChecks++;
    }
    if (numberOfHealthChecks > 1) {
        throw new IllegalArgumentException("Only one health check may be specified.");
    }
    if (execHealthCheck != null && !execHealthCheck.isEmpty()) {
        builder.setHealthCheck(ExecHealthCheck.of(execHealthCheck));
    } else if (!isNullOrEmpty(httpHealthCheck)) {
        final String[] parts = httpHealthCheck.split(":", 2);
        if (parts.length != 2) {
            throw new IllegalArgumentException("Invalid HTTP health check: " + httpHealthCheck);
        }
        builder.setHealthCheck(HttpHealthCheck.of(parts[0], parts[1]));
    } else if (!isNullOrEmpty(tcpHealthCheck)) {
        builder.setHealthCheck(TcpHealthCheck.of(tcpHealthCheck));
    }
    final List<String> securityOpt = options.getList(securityOptArg.getDest());
    if (securityOpt != null && !securityOpt.isEmpty()) {
        builder.setSecurityOpt(securityOpt);
    }
    final String networkMode = options.getString(networkModeArg.getDest());
    if (!isNullOrEmpty(networkMode)) {
        builder.setNetworkMode(networkMode);
    }
    final String token = options.getString(tokenArg.getDest());
    if (!isNullOrEmpty(token)) {
        builder.setToken(token);
    }
    final List<String> addCaps = options.getList(addCapabilityArg.getDest());
    if (addCaps != null && !addCaps.isEmpty()) {
        builder.setAddCapabilities(addCaps);
    }
    final List<String> dropCaps = options.getList(dropCapabilityArg.getDest());
    if (dropCaps != null && !dropCaps.isEmpty()) {
        builder.setDropCapabilities(dropCaps);
    }
    // We build without a hash here because we want the hash to be calculated server-side.
    // This allows different CLI versions to be cross-compatible with different master versions
    // that have either more or fewer job parameters.
    final Job job = builder.buildWithoutHash();
    final Collection<String> errors = JOB_VALIDATOR.validate(job);
    if (!errors.isEmpty()) {
        if (!json) {
            for (final String error : errors) {
                out.println(error);
            }
        } else {
            final CreateJobResponse createJobResponse = new CreateJobResponse(CreateJobResponse.Status.INVALID_JOB_DEFINITION, ImmutableList.copyOf(errors), job.getId().toString());
            out.println(createJobResponse.toJsonString());
        }
        return 1;
    }
    if (!quiet && !json) {
        out.println("Creating job: " + job.toJsonString());
    }
    final CreateJobResponse status = client.createJob(job).get();
    if (status.getStatus() == CreateJobResponse.Status.OK) {
        if (!quiet && !json) {
            out.println("Done.");
        }
        if (json) {
            out.println(status.toJsonString());
        } else {
            out.println(status.getId());
        }
        return 0;
    } else {
        if (!quiet && !json) {
            out.println("Failed: " + status);
        } else if (json) {
            out.println(status.toJsonString());
        }
        return 1;
    }
}
Also used : Pattern(java.util.regex.Pattern) Matcher(java.util.regex.Matcher) DateTime(org.joda.time.DateTime) ServiceEndpoint(com.spotify.helios.common.descriptors.ServiceEndpoint) CreateJobResponse(com.spotify.helios.common.protocol.CreateJobResponse) ServicePorts(com.spotify.helios.common.descriptors.ServicePorts) PortMapping(com.spotify.helios.common.descriptors.PortMapping) Job(com.spotify.helios.common.descriptors.Job) File(java.io.File) Map(java.util.Map) ImmutableMap(com.google.common.collect.ImmutableMap) HashMap(java.util.HashMap) ServiceEndpoint(com.spotify.helios.common.descriptors.ServiceEndpoint)

Example 4 with ServiceEndpoint

use of com.spotify.helios.common.descriptors.ServiceEndpoint in project helios by spotify.

the class JobValidator method validate.

public Set<String> validate(final Job job) {
    final Set<String> errors = Sets.newHashSet();
    errors.addAll(validateJobId(job));
    errors.addAll(validateJobImage(job.getImage()));
    errors.addAll(validateJobHostName(job.getHostname()));
    // Check that there's not external port collision
    final Set<Integer> externalPorts = Sets.newHashSet();
    for (final PortMapping mapping : job.getPorts().values()) {
        final Integer externalMappedPort = mapping.getExternalPort();
        if (externalPorts.contains(externalMappedPort) && externalMappedPort != null) {
            errors.add(format("Duplicate external port mapping: %s", externalMappedPort));
        }
        externalPorts.add(externalMappedPort);
    }
    // Verify port mappings
    for (final Map.Entry<String, PortMapping> entry : job.getPorts().entrySet()) {
        final String name = entry.getKey();
        final PortMapping mapping = entry.getValue();
        if (!PORT_MAPPING_PROTO_PATTERN.matcher(mapping.getProtocol()).matches()) {
            errors.add(format("Invalid port mapping protocol: %s", mapping.getProtocol()));
        }
        if (!legalPort(mapping.getInternalPort())) {
            errors.add(format("Invalid internal port: %d", mapping.getInternalPort()));
        }
        if (mapping.getExternalPort() != null && !legalPort(mapping.getExternalPort())) {
            errors.add(format("Invalid external port: %d", mapping.getExternalPort()));
        }
        if (!PORT_MAPPING_NAME_PATTERN.matcher(name).matches()) {
            errors.add(format("Invalid port mapping endpoint name: %s", name));
        }
    }
    // Verify service registrations
    for (final ServiceEndpoint registration : job.getRegistration().keySet()) {
        final ServicePorts servicePorts = job.getRegistration().get(registration);
        if (servicePorts == null || servicePorts.getPorts() == null) {
            errors.add(format("registration for '%s' is malformed: does not have a port mapping", registration.getName()));
            continue;
        }
        for (final String portName : servicePorts.getPorts().keySet()) {
            if (!job.getPorts().containsKey(portName)) {
                errors.add(format("Service registration refers to missing port mapping: %s=%s", registration, portName));
            }
            if (!REGISTRATION_NAME_PATTERN.matcher(registration.getName()).matches()) {
                errors.add(format("Invalid service registration name: %s", registration.getName()));
            }
        }
    }
    // Validate volumes
    for (final Map.Entry<String, String> entry : job.getVolumes().entrySet()) {
        final String path = entry.getKey();
        final String source = entry.getValue();
        if (!path.startsWith("/")) {
            errors.add("Volume path is not absolute: " + path);
            continue;
        }
        if (!isNullOrEmpty(source) && !source.startsWith("/")) {
            errors.add("Volume source is not absolute: " + source);
            continue;
        }
        final String[] parts = path.split(":", 3);
        if (path.isEmpty() || path.equals("/") | parts.length > 2 || (parts.length > 1 && parts[1].isEmpty())) {
            errors.add(format("Invalid volume path: %s", path));
        }
    }
    // Validate Expiry
    final Date expiry = job.getExpires();
    if (expiry != null && expiry.before(new Date())) {
        errors.add("Job expires in the past");
    }
    errors.addAll(validateJobHealthCheck(job));
    errors.addAll(validateJobNetworkMode(job));
    if (shouldValidateAddCapabilities) {
        errors.addAll(validateAddCapabilities(job));
    }
    return errors;
}
Also used : ServicePorts(com.spotify.helios.common.descriptors.ServicePorts) PortMapping(com.spotify.helios.common.descriptors.PortMapping) Map(java.util.Map) ServiceEndpoint(com.spotify.helios.common.descriptors.ServiceEndpoint) Date(java.util.Date)

Example 5 with ServiceEndpoint

use of com.spotify.helios.common.descriptors.ServiceEndpoint in project helios by spotify.

the class JobValidatorTest method testValidPortTagsPass.

@Test
public void testValidPortTagsPass() {
    final Job j = Job.newBuilder().setName("foo").setVersion("1").setImage("foobar").build();
    final Job.Builder builder = j.toBuilder();
    final Map<String, PortMapping> ports = ImmutableMap.of("add_ports1", PortMapping.of(1234), "add_ports2", PortMapping.of(2345));
    final ImmutableMap.Builder<String, ServicePortParameters> servicePortsBuilder = ImmutableMap.builder();
    servicePortsBuilder.put("add_ports1", new ServicePortParameters(ImmutableList.of("tag1", "tag2")));
    servicePortsBuilder.put("add_ports2", new ServicePortParameters(ImmutableList.of("tag3", "tag4")));
    final ServicePorts servicePorts = new ServicePorts(servicePortsBuilder.build());
    final Map<ServiceEndpoint, ServicePorts> addRegistration = ImmutableMap.of(ServiceEndpoint.of("add_service", "add_proto"), servicePorts);
    builder.setPorts(ports).setRegistration(addRegistration);
    assertThat(validator.validate(builder.build()), is(empty()));
}
Also used : ServicePorts(com.spotify.helios.common.descriptors.ServicePorts) ServicePortParameters(com.spotify.helios.common.descriptors.ServicePortParameters) Matchers.containsString(org.hamcrest.Matchers.containsString) PortMapping(com.spotify.helios.common.descriptors.PortMapping) Job(com.spotify.helios.common.descriptors.Job) ImmutableMap(com.google.common.collect.ImmutableMap) ServiceEndpoint(com.spotify.helios.common.descriptors.ServiceEndpoint) Test(org.junit.Test)

Aggregations

PortMapping (com.spotify.helios.common.descriptors.PortMapping)9 ServiceEndpoint (com.spotify.helios.common.descriptors.ServiceEndpoint)9 ServicePorts (com.spotify.helios.common.descriptors.ServicePorts)9 Job (com.spotify.helios.common.descriptors.Job)6 Test (org.junit.Test)6 ImmutableMap (com.google.common.collect.ImmutableMap)3 JobId (com.spotify.helios.common.descriptors.JobId)3 File (java.io.File)3 Map (java.util.Map)3 HeliosClient (com.spotify.helios.client.HeliosClient)2 ServicePortParameters (com.spotify.helios.common.descriptors.ServicePortParameters)2 ServiceRegistration (com.spotify.helios.serviceregistration.ServiceRegistration)2 FileOutputStream (java.io.FileOutputStream)2 Matchers.containsString (org.hamcrest.Matchers.containsString)2 JobStatus (com.spotify.helios.common.descriptors.JobStatus)1 CreateJobResponse (com.spotify.helios.common.protocol.CreateJobResponse)1 Endpoint (com.spotify.helios.serviceregistration.ServiceRegistration.Endpoint)1 Path (java.nio.file.Path)1 Date (java.util.Date)1 HashMap (java.util.HashMap)1