com.spotify.helios.cli.command.JobCreateCommandTest.java Source code

Java tutorial

Introduction

Here is the source code for com.spotify.helios.cli.command.JobCreateCommandTest.java

Source

/*
 * Copyright (c) 2014 Spotify AB.
 *
 * Licensed 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 com.spotify.helios.cli.command;

import com.spotify.helios.client.HeliosClient;
import com.spotify.helios.common.descriptors.Job;
import com.spotify.helios.common.protocol.CreateJobResponse;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;

import net.sourceforge.argparse4j.ArgumentParsers;
import net.sourceforge.argparse4j.inf.ArgumentParser;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;

import org.hamcrest.CustomTypeSafeMatcher;
import org.hamcrest.Matcher;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ExecutionException;

import static com.google.common.util.concurrent.Futures.immediateFuture;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.hasItem;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.assertEquals;
import static org.mockito.Matchers.argThat;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

public class JobCreateCommandTest {

    private static final Logger log = LoggerFactory.getLogger(JobCreateCommandTest.class);

    private static final String JOB_NAME = "foo";
    private static final String JOB_ID = JOB_NAME + ":123";
    private static final String EXEC_HEALTH_CHECK = "touch /this";
    private static final List<String> SECURITY_OPT = ImmutableList.of("label:user:dxia", "apparmor:foo");
    private static final String NETWORK_MODE = "host";

    private final Namespace options = mock(Namespace.class);
    private final HeliosClient client = mock(HeliosClient.class);
    private final ByteArrayOutputStream baos = new ByteArrayOutputStream();
    private final PrintStream out = new PrintStream(baos);

    private JobCreateCommand command;

    private Map<String, String> envVars = Maps.newHashMap();

    @Before
    public void setUp() {
        // use a real, dummy Subparser impl to avoid having to mock out every single call
        final ArgumentParser parser = ArgumentParsers.newArgumentParser("test");
        final Subparser subparser = parser.addSubparsers().addParser("create");

        final Supplier<Map<String, String>> envVarSupplier = new Supplier<Map<String, String>>() {
            @Override
            public Map<String, String> get() {
                return ImmutableMap.copyOf(envVars);
            }
        };

        command = new JobCreateCommand(subparser, envVarSupplier);

        when(client.createJob(argThat(matchesName(JOB_NAME)))).thenReturn(immediateFuture(
                new CreateJobResponse(CreateJobResponse.Status.OK, Collections.<String>emptyList(), "12345")));
    }

    private int runCommand() throws InterruptedException, ExecutionException, IOException {
        return runCommand(false);
    }

    private int runCommand(boolean json) throws InterruptedException, ExecutionException, IOException {
        final int ret = command.run(options, client, out, json, null);
        log.debug("Output from command: [{}]", baos.toString().replaceAll("\n", "\\\\n"));
        return ret;
    }

    @Test
    public void testValidJobCreateCommand() throws Exception {
        when(options.getString("id")).thenReturn(JOB_ID);
        when(options.getString("image")).thenReturn("busybox:latest");
        when(options.getString("exec_check")).thenReturn(EXEC_HEALTH_CHECK);
        // For some reason the mocked options.getInt() returns 0 by default.
        // Explicitly return null to check that the value from the JSON file doesn't get overwritten.
        when(options.getInt("grace_period")).thenReturn(null);
        // TODO (mbrown): this path is weird when running from IntelliJ, should be changed to not
        // care about CWD
        doReturn(new File("src/test/resources/job_config.json")).when(options).get("file");
        doReturn(SECURITY_OPT).when(options).getList("security_opt");
        when(options.getString("network_mode")).thenReturn(NETWORK_MODE);
        when(options.getList("metadata")).thenReturn(Lists.<Object>newArrayList("a=1", "b=2"));
        doReturn(ImmutableList.of("cap1", "cap2")).when(options).getList("add_capability");
        doReturn(ImmutableList.of("cap3", "cap4")).when(options).getList("drop_capability");

        final int ret = runCommand();

        assertEquals(0, ret);
        final String output = baos.toString();
        assertThat(output, containsString("\"created\":null"));
        assertThat(output, containsString("\"env\":{\"JVM_ARGS\":\"-Ddw.feature.randomFeatureFlagEnabled=true\"}"));
        assertThat(output, containsString("\"metadata\":{\"a\":\"1\",\"b\":\"2\"},"));
        assertThat(output, containsString("\"gracePeriod\":100"));
        assertThat(output, containsString(
                "\"healthCheck\":{\"type\":\"exec\"," + "\"command\":[\"touch\",\"/this\"],\"type\":\"exec\"},"));
        assertThat(output, containsString("\"securityOpt\":[\"label:user:dxia\",\"apparmor:foo\"]"));
        assertThat(output, containsString("\"networkMode\":\"host\""));
        assertThat(output, containsString("\"expires\":null"));
        assertThat(output, containsString("\"addCapabilities\":[\"cap1\",\"cap2\"]"));
        assertThat(output, containsString("\"dropCapabilities\":[\"cap3\",\"cap4\"]"));
    }

    @Test
    public void testJobCreateCommandFailsWithInvalidJobID() throws Exception {
        when(options.getString("id")).thenReturn(JOB_NAME);
        when(options.getString("image")).thenReturn("busybox:latest");
        final int ret = runCommand();
        assertEquals(1, ret);
    }

    @Test
    public void testJobCreateCommandFailsWithInvalidPortProtocol() throws Exception {
        when(options.getString("id")).thenReturn(JOB_ID);
        when(options.getString("image")).thenReturn("busybox");
        doReturn(ImmutableList.of("dns=53:53/http")).when(options).getList("port");
        final int ret = runCommand(true);

        assertEquals(1, ret);
        final String output = baos.toString();
        assertThat(output, containsString("\"status\":\"INVALID_JOB_DEFINITION\"}"));
        assertThat(output, containsString("\"errors\":[\"Invalid port mapping protocol: http\"]"));
    }

    @Test(expected = IllegalArgumentException.class)
    public void testJobCreateCommandFailsWithInvalidFilePath() throws Exception {
        when(options.getString("id")).thenReturn(JOB_ID);
        when(options.getString("image")).thenReturn("busybox:latest");
        doReturn(new File("non/existant/file")).when(options).get("file");
        runCommand();
    }

    /**
     * Test that when the environment variables we have defaults for are set, they are picked up in
     * the job metadata.
     */
    @Test
    public void testMetadataPicksUpEnvVars() throws Exception {
        envVars.put("GIT_COMMIT", "abcdef1234");

        when(options.getString("id")).thenReturn(JOB_ID);
        when(options.getString("image")).thenReturn("busybox:latest");

        when(options.getList("metadata")).thenReturn(Lists.<Object>newArrayList("foo=bar"));

        final int ret = runCommand();

        assertEquals(0, ret);
        final String output = baos.toString();

        final String expectedOutputPrefix = "Creating job: ";
        assertThat(output, startsWith(expectedOutputPrefix));

        final ObjectMapper objectMapper = new ObjectMapper();

        final String jsonFromOutput = output.split("\n")[0].substring(expectedOutputPrefix.length());
        final JsonNode jsonNode = objectMapper.readTree(jsonFromOutput);

        final ArrayList<String> fieldNames = Lists.newArrayList(jsonNode.fieldNames());
        assertThat(fieldNames, hasItem("metadata"));

        assertThat(jsonNode.get("metadata").isObject(), equalTo(true));

        final ObjectNode metadataNode = (ObjectNode) jsonNode.get("metadata");
        assertThat(metadataNode.get("foo").asText(), equalTo("bar"));
        assertThat(metadataNode.get("GIT_COMMIT").asText(), equalTo("abcdef1234"));
    }

    private CustomTypeSafeMatcher<Job> matchesName(final String name) {
        return new CustomTypeSafeMatcher<Job>("A Job with name " + name) {
            @Override
            protected boolean matchesSafely(final Job item) {
                return item.getId().getName().equals(name);
            }
        };
    }

    /**
     * Ensure that creating a job from a json file which has added and dropped capabilities is not
     * overwritten by empty arguments in the CLI switches.
     */
    @Test
    public void testAddCapabilitiesFromJsonFile() throws Exception {
        when(options.getString("id")).thenReturn(JOB_ID);
        when(options.getString("image")).thenReturn("foobar");

        when(options.get("file")).thenReturn(new File("src/test/resources/job_config_extra_capabilities.json"));

        when(options.getList("add-capability")).thenReturn(Collections.emptyList());
        when(options.getList("drop-capability")).thenReturn(Collections.emptyList());

        assertEquals(0, runCommand());

        verify(client).createJob(argThat(
                hasCapabilities(ImmutableSet.of("cap_one", "cap_two"), ImmutableSet.of("cap_three", "cap_four"))));
    }

    private Matcher<Job> hasCapabilities(final Set<String> added, final Set<String> dropped) {
        final String description = "Job with addCapabilities=" + added + " and droppedCapabilities=" + dropped;

        return new CustomTypeSafeMatcher<Job>(description) {
            @Override
            protected boolean matchesSafely(final Job actual) {
                return Objects.equals(added, actual.getAddCapabilities())
                        && Objects.equals(dropped, actual.getDropCapabilities());
            }
        };
    }

}