Skip to content

Commit

Permalink
[#679] Introduce status command (#686)
Browse files Browse the repository at this point in the history
Fixes #679
  • Loading branch information
lucapette committed Jan 20, 2021
1 parent 2c26a9d commit 5ef51d5
Show file tree
Hide file tree
Showing 36 changed files with 333 additions and 194 deletions.
@@ -1,46 +1,25 @@
package co.airy.core.api.config;

import co.airy.core.api.admin.payload.ClientConfigResponsePayload;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import java.util.ArrayList;
import java.util.List;
import java.util.HashMap;
import java.util.Map;

@RestController
public class ClientConfigController {

private final ServiceDiscovery serviceDiscovery;
private final String namespace;
private final RestTemplate restTemplate;

public ClientConfigController(ServiceDiscovery serviceDiscovery, @Value("${kubernetes.namespace}") String namespace, RestTemplate restTemplate) {
public ClientConfigController(ServiceDiscovery serviceDiscovery) {
this.serviceDiscovery = serviceDiscovery;
this.namespace = namespace;
this.restTemplate = restTemplate;
}

@PostMapping("/client.config")
public ResponseEntity<ClientConfigResponsePayload> getConfig() {
List<Map<String, Map<String, String>>> components = new ArrayList<>();

for (String service : serviceDiscovery.getServices()) {
try {
ResponseEntity<Object> response = restTemplate.exchange(String.format("http://%s.%s/actuator/health", service, namespace), HttpMethod.GET, null, Object.class);
components.add(Map.of(service.replace("-connector", ""), Map.of("enabled", Boolean.toString(response.getStatusCode().is2xxSuccessful()))));
} catch (Exception e) {
components.add(Map.of(service.replace("-connector", ""), Map.of("enabled", Boolean.toString(false))));
}
}

return ResponseEntity.ok(ClientConfigResponsePayload.builder()
.components(components)
.features(List.of())
.components(serviceDiscovery.getComponents())
.features(Map.of())
.build());
}
}
@@ -1,18 +1,17 @@
package co.airy.core.api.admin.payload;
package co.airy.core.api.config;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;
import java.util.Map;

@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ClientConfigResponsePayload {
private List<Map<String, Map<String, String>>> components;
private List<Map<String, String>> features;
private Map<String, Map<String, Object>> components;
private Map<String, String> features;
}
@@ -0,0 +1,15 @@
package co.airy.core.api.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.web.client.RestTemplate;

@EnableScheduling
@Configuration
public class ClientControllerConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
@@ -1,26 +1,50 @@
package co.airy.core.api.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;

import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Component
public class ServiceDiscovery {
private final String namespace;
private final RestTemplate restTemplate;

private final Map<String, Map<String, Object>> components = new ConcurrentHashMap<>();

private static final List<String> services = List.of(
"sources-chatplugin",
"sources-facebook-connector",
"sources-twilio-connector",
"sources-google-connector"
);

@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
public ServiceDiscovery(@Value("${kubernetes.namespace}") String namespace, RestTemplate restTemplate) {
this.namespace = namespace;
this.restTemplate = restTemplate;
}

public Map<String, Map<String, Object>> getComponents() {
return components;
}

public List<String> getServices() {
return services;
@Scheduled(fixedRate = 1_000)
private void updateComponentsStatus() {
for (String service : services) {
try {
ResponseEntity<Object> response = restTemplate.exchange(String.format("http://%s.%s/actuator/health", service, namespace), HttpMethod.GET, null, Object.class);
components.put(service.replace("-connector", ""), Map.of("enabled", response.getStatusCode().is2xxSuccessful()));
} catch (Exception e) {
components.put(service.replace("-connector", ""), Map.of("enabled",false));
}
}
}
}
@@ -1,6 +1,5 @@
package co.airy.core.api.admin;
package co.airy.core.api.config;

import co.airy.core.api.config.ClientConfigController;
import co.airy.kafka.schema.application.ApplicationCommunicationChannels;
import co.airy.kafka.schema.application.ApplicationCommunicationTags;
import co.airy.kafka.schema.application.ApplicationCommunicationWebhooks;
Expand All @@ -22,15 +21,20 @@
import org.springframework.http.HttpStatus;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.client.ExpectedCount;
import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.web.client.RestTemplate;

import java.net.URI;

import static co.airy.test.Timing.retryOnException;
import static org.hamcrest.CoreMatchers.everyItem;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.collection.IsCollectionWithSize.hasSize;
import static org.springframework.test.web.client.ExpectedCount.min;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.method;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = AirySpringBootApplication.class)
Expand Down Expand Up @@ -81,23 +85,27 @@ void beforeEach() throws Exception {

@Test
public void canReturnConfig() throws Exception {
mockServer.expect(requestTo(new URI("http://sources-chatplugin.default/actuator/health")))
mockServer.expect(min(1), requestTo(new URI("http://sources-chatplugin.default/actuator/health")))
.andExpect(method(HttpMethod.GET))
.andRespond(withStatus(HttpStatus.OK));

mockServer.expect(requestTo(new URI("http://sources-facebook-connector.default/actuator/health")))
mockServer.expect(min(1), requestTo(new URI("http://sources-facebook-connector.default/actuator/health")))
.andExpect(method(HttpMethod.GET))
.andRespond(withStatus(HttpStatus.OK));

mockServer.expect(requestTo(new URI("http://sources-twilio-connector.default/actuator/health")))
mockServer.expect(min(1), requestTo(new URI("http://sources-twilio-connector.default/actuator/health")))
.andExpect(method(HttpMethod.GET))
.andRespond(withStatus(HttpStatus.OK));

mockServer.expect(requestTo(new URI("http://sources-google-connector.default/actuator/health")))
mockServer.expect(min(1), requestTo(new URI("http://sources-google-connector.default/actuator/health")))
.andExpect(method(HttpMethod.GET))
.andRespond(withStatus(HttpStatus.OK));

webTestHelper.post("/client.config", "{}", "user-id").andExpect(status().isOk());
retryOnException(() -> webTestHelper.post("/client.config", "{}", "user-id")
.andExpect(status().isOk())
.andExpect(jsonPath("$.components.*", hasSize(4)))
.andExpect(jsonPath("$.components.*.enabled", everyItem(is(true)))),
"client.config call failed");
}

}
3 changes: 2 additions & 1 deletion infrastructure/cli/cmd/BUILD
Expand Up @@ -10,8 +10,9 @@ go_library(
"CommitSHA1": "{STABLE_GIT_COMMIT}",
},
deps = [
"//infrastructure/cli/cmd/auth",
"//infrastructure/cli/cmd/api",
"//infrastructure/cli/cmd/config",
"//infrastructure/cli/cmd/status",
"//infrastructure/cli/cmd/ui",
"@com_github_mitchellh_go_homedir//:go-homedir",
"@com_github_spf13_cobra//:cobra",
Expand Down
@@ -1,9 +1,13 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")

go_library(
name = "auth",
srcs = ["auth.go"],
importpath = "cli/cmd/auth",
name = "api",
srcs = [
"api.go",
"login.go",
"signup.go",
],
importpath = "cli/cmd/api",
visibility = ["//visibility:public"],
deps = [
"//lib/go/httpclient",
Expand Down
18 changes: 18 additions & 0 deletions infrastructure/cli/cmd/api/api.go
@@ -0,0 +1,18 @@
package api

import (
"github.com/spf13/cobra"
)

// APICmd subcommand for Airy Core
var APICmd = &cobra.Command{
Use: "api",
TraverseChildren: true,
Short: "Interacts with the Airy Core Platform HTTP API",
Long: ``,
}

func init() {
APICmd.AddCommand(signupCmd)
APICmd.AddCommand(loginCmd)
}
42 changes: 42 additions & 0 deletions infrastructure/cli/cmd/api/login.go
@@ -0,0 +1,42 @@
package api

import (
"fmt"
"os"

"github.com/airyhq/airy/lib/go/httpclient"
"github.com/airyhq/airy/lib/go/httpclient/payloads"

"github.com/spf13/cobra"
"github.com/spf13/viper"
)

var loginCmd = &cobra.Command{
Use: "login",
Short: "Logs you in in the Airy Core Platform",
Long: ``,
Run: login,
}

func login(cmd *cobra.Command, args []string) {
email, _ := cmd.Flags().GetString("email")
password, _ := cmd.Flags().GetString("password")
c := httpclient.NewClient(viper.GetString("apihost"))

loginRequestPayload := payloads.LoginRequestPayload{Email: email, Password: password}
res, err := c.Login(loginRequestPayload)
if err != nil {
fmt.Println("could not login:", err)
os.Exit(1)
}
fmt.Printf("logged in correctly: %s\n", res.Token)

viper.Set("apiJWTToken", res.Token)
viper.WriteConfig()
}

func init() {
var email, password string
loginCmd.Flags().StringVarP(&email, "email", "e", "grace@hopper.com", "Email")
loginCmd.Flags().StringVarP(&password, "password", "p", "the_answer_is_42", "Password")
}
43 changes: 43 additions & 0 deletions infrastructure/cli/cmd/api/signup.go
@@ -0,0 +1,43 @@
package api

import (
"fmt"
"os"

"github.com/airyhq/airy/lib/go/httpclient"
"github.com/airyhq/airy/lib/go/httpclient/payloads"

"github.com/spf13/cobra"
"github.com/spf13/viper"
)

var signupCmd = &cobra.Command{
Use: "signup",
Short: "Signs users up in the Airy Core Platform",
Long: ``,
Run: signup,
}

func signup(cmd *cobra.Command, args []string) {
firstName, _ := cmd.Flags().GetString("firstName")
lastName, _ := cmd.Flags().GetString("lastName")
email, _ := cmd.Flags().GetString("email")
password, _ := cmd.Flags().GetString("password")
c := httpclient.NewClient(viper.GetString("apihost"))

signupRequestPayload := payloads.SignupRequestPayload{FirstName: firstName, LastName: lastName, Email: email, Password: password}
res, err := c.Signup(signupRequestPayload)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
fmt.Printf("user created: %s\n", res.ID)
}

func init() {
var firstName, lastName, email, password string
signupCmd.Flags().StringVarP(&firstName, "firstName", "f", "Grace", "First name")
signupCmd.Flags().StringVarP(&lastName, "lastName", "l", "Hopper", "Last name")
signupCmd.Flags().StringVarP(&email, "email", "e", "grace@hopper.com", "Email")
signupCmd.Flags().StringVarP(&password, "password", "p", "the_answer_is_42", "Password")
}
49 changes: 0 additions & 49 deletions infrastructure/cli/cmd/auth/auth.go

This file was deleted.

0 comments on commit 5ef51d5

Please sign in to comment.