Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Grafana api #86

Merged
merged 8 commits into from
Oct 25, 2016
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,7 @@ Test your setup http://asquera.de/blog/2013-07-10/an-elasticsearch-workflow/
## How to setup MYSQL locally

`docker run -e MYSQL_ROOT_PASSWORD=my-secret-pw -e MYSQL_DATABASE=flowupdb -e MYSQL_USER=flowupUser -e MYSQL_PASSWORD=flowupPassword -p 127.0.0.1:3306:3306 -d mysql:5.6`

## How to setup grafana locally

`docker run -p 3000:3000 -d grafana/grafana`
7 changes: 6 additions & 1 deletion app/Module.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import com.google.inject.AbstractModule;
import com.google.inject.name.Names;
import datasources.ElasticSearchDatasource;
import datasources.elasticsearch.ElasticSearchDatasource;
import play.Configuration;
import play.Environment;
import usecases.MetricsDatasource;
Expand All @@ -27,5 +27,10 @@ protected void configure() {
bind(MetricsDatasource.class)
.to(ElasticSearchDatasource.class)
.asEagerSingleton();

Configuration grafanaConf = configuration.getConfig("grafana");

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

have sense move this grafana literal to a constant?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will do it, it feels like a java thing to me :D
have sense

bind(Configuration.class)
.annotatedWith(Names.named("grafana"))
.toInstance(grafanaConf);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package datasources;
package datasources.elasticsearch;

import lombok.Data;
import org.jetbrains.annotations.Nullable;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package datasources;
package datasources.elasticsearch;


import com.fasterxml.jackson.annotation.JsonProperty;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package datasources;
package datasources.elasticsearch;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package datasources;
package datasources.elasticsearch;

import lombok.Data;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package datasources;
package datasources.elasticsearch;

import com.fasterxml.jackson.databind.node.ObjectNode;
import play.libs.F;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package datasources;
package datasources.elasticsearch;

import com.fasterxml.jackson.databind.JsonNode;
import org.apache.commons.lang3.StringUtils;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package datasources;
package datasources.elasticsearch;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package datasources;
package datasources.elasticsearch;

import com.fasterxml.jackson.databind.JsonNode;
import lombok.Data;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package datasources;
package datasources.elasticsearch;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
Expand Down
114 changes: 114 additions & 0 deletions app/datasources/grafana/GrafanaClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package datasources.grafana;

import com.fasterxml.jackson.databind.node.ObjectNode;
import models.Organization;
import models.User;
import org.apache.commons.lang3.RandomStringUtils;
import org.jetbrains.annotations.NotNull;
import play.Configuration;
import play.Logger;
import play.libs.Json;
import play.libs.ws.WSClient;
import play.libs.ws.WSRequest;
import play.mvc.Http;

import javax.inject.Inject;
import javax.inject.Named;
import java.security.SecureRandom;
import java.util.UUID;
import java.util.concurrent.CompletionStage;

public class GrafanaClient {

private final WSClient ws;
private final String baseUrl;
private final String apiKey;
private final String adminUser;
private final String adminPassword;

@Inject
public GrafanaClient(WSClient ws, @Named("grafana") Configuration grafanaConf) {
this.ws = ws;

this.apiKey = grafanaConf.getString("api_key");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So are we going to use the api_key at the end?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to, but there is something wrong in grafana that does not let me.
I will open a ticket and fix that later.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

this.adminUser = grafanaConf.getString("admin_user");
this.adminPassword = grafanaConf.getString("admin_password");

String scheme = grafanaConf.getString("scheme");
String host = grafanaConf.getString("host");
String port = grafanaConf.getString("port");
this.baseUrl = scheme + "://" + host + ":" + port;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Java has a URL class you can use if I'm not wrong.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes but the Play client only accept String, I don't know if it's worth the double transformation, String -> URL -> String

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can pass from string to URL and URL to string. The only benefit you are going to have is validating the URL in construction, but it's not so important here :)

}

public CompletionStage<GrafanaResponse> createUser(final User user) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are you using any kind of checkstyle? do you have a typo here, a double space between public and return value.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no checkstyle in this project I will add one later.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

try to add as soon as possible because fix check style after having a lot of code could be a pain in the ass. @pedrovgs or I can help you with this one.

String adminUserEndpoint = "/api/admin/users";

String grafanaPassword = generatePassword();
ObjectNode userRequest = Json.newObject()
.put("name", user.getName())

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

have sense move this request properties to constants?

.put("email", user.getEmail())
.put("password", grafanaPassword);

UUID userId = user.getId();

Logger.debug(userRequest.toString());

return getWsRequestForUserCreation(adminUserEndpoint).post(userRequest).thenApply(response -> {
Logger.debug(response.getBody());
if (response.getStatus() == Http.Status.OK) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we need to do something if the request fails?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sound good move this ok to a method no?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pedrovgs I created a ticket #88, for the MVP I don't want to handle the failure of grafana api.
@flipper83 👍

String id = response.asJson().get("id").toString();
User createdUser = User.find.byId(userId);
createdUser.setGrafanaUserId(id);
createdUser.setGrafanaPassword(grafanaPassword);
createdUser.save();
}
return Json.fromJson(response.asJson(), GrafanaResponse.class);
});
}

public CompletionStage<GrafanaResponse> addUserToOrganisation(User user, Organization organization) {
String adminUserEndpoint = "/api/orgs/:orgId/users".replaceFirst(":orgId", organization.grafanaId);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you move those urls to a constants?


ObjectNode userRequest = Json.newObject()
.put("loginOrEmail", user.getEmail())
.put("role", "Viewer");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you don't want to create json manually you can create a class as you tend to do with the responses :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I no strong opinion about that, coming from swift, this seems fine, until I need this model more than once.


UUID userId = user.getId();

Logger.debug(userRequest.toString());

return getWsRequestForUserCreation(adminUserEndpoint).post(userRequest).thenApply(response -> {
Logger.debug(response.getBody());
return Json.fromJson(response.asJson(), GrafanaResponse.class);
});
}

public CompletionStage<GrafanaResponse> deleteUserInDefaultOrganisation(User user) {
String adminUserEndpoint = "/api/orgs/:orgId/users/:userId".replaceFirst(":orgId", "1").replaceFirst(":userId", user.getGrafanaUserId());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As this string is repeated in some methods we could extract it into a constant.

Copy link
Member Author

@davideme davideme Oct 25, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍


return getWsRequestForUserCreation(adminUserEndpoint).delete().thenApply(response -> {
Logger.debug(response.getBody());
return Json.fromJson(response.asJson(), GrafanaResponse.class);
});
}

private WSRequest getWsRequestForUserCreation(String adminUserEndpoint) {
WSRequest wsRequest = ws.url(baseUrl + adminUserEndpoint).setHeader("Accept", "application/json").setContentType("application/json")
.setAuth(this.adminUser, this.adminPassword);
Logger.debug(wsRequest.getHeaders().toString());
return wsRequest;
}

private WSRequest getWsRequest(String adminUserEndpoint) {
WSRequest wsRequest = ws.url(baseUrl + adminUserEndpoint).setHeader("Accept", "application/json").setContentType("application/json")
.setHeader("Authorization", "Bearer " + this.apiKey);
Logger.debug(wsRequest.getHeaders().toString());
return wsRequest;
}

@NotNull
private String generatePassword() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice password generator!! I'd move this method to a PasswordGeneratorclass just to don't mix responsibilities between this api client and the pass generator :)

String characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789~`!@#$%^&*()-_=+[{]}\\|;:\'\",<.>/?";
return RandomStringUtils.random(255, 0, 0, false, false, characters.toCharArray(), new SecureRandom());
}
}
9 changes: 9 additions & 0 deletions app/datasources/grafana/GrafanaResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package datasources.grafana;


import lombok.Data;

@Data
public class GrafanaResponse {
private String message;
}
12 changes: 12 additions & 0 deletions app/models/Organization.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package models;

import com.avaje.ebean.ExpressionList;
import com.avaje.ebean.Model;

import javax.persistence.Entity;
Expand All @@ -22,5 +23,16 @@ public class Organization {
@ManyToMany
public List<User> members;

public String grafanaId;

public static Model.Finder<UUID, Organization> find = new Model.Finder<>(Organization.class);

public static Organization findByGoogleAccount(String googleAccount) {
return getGoogleAccountUserFind(googleAccount).findUnique();
}

private static ExpressionList<Organization> getGoogleAccountUserFind(String googleAccount) {
return find.where().eq("google_account", googleAccount);
}

}
18 changes: 11 additions & 7 deletions app/models/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,28 @@
@Entity
public class User extends Model {
@Id
public UUID id;
private UUID id;

@Constraints.Email
@Column(unique = true)
public String email;
private String email;

@Constraints.Required
public String name;
private String name;

public boolean active;
private boolean active;

public boolean emailValidated;
private boolean emailValidated;

private String grafanaUserId;

private String grafanaPassword;

@OneToMany(cascade = CascadeType.ALL)
public List<LinkedAccount> linkedAccounts;
private List<LinkedAccount> linkedAccounts;

@ManyToMany(mappedBy = "members")
public List<Organization> organizations;
private List<Organization> organizations;

public static final Finder<UUID, User> find = new Finder<>(User.class);

Expand Down
2 changes: 0 additions & 2 deletions app/module/AuthenticationModule.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package module;

import com.feth.play.module.pa.Resolver;
import com.feth.play.module.pa.providers.oauth2.github.GithubAuthProvider;
import com.feth.play.module.pa.providers.oauth2.google.GoogleAuthProvider;
import com.google.inject.AbstractModule;
import service.AuthenticationResolver;
Expand All @@ -13,7 +12,6 @@ protected void configure() {
// play-authenticate dependencies
bind(Resolver.class).to(AuthenticationResolver.class);
// Following class depend on PlayAuthenticate auth, and they self register to it.
bind(GithubAuthProvider.class).asEagerSingleton();
bind(GoogleAuthProvider.class).asEagerSingleton();
bind(FlowUpUserService.class).asEagerSingleton();
}
Expand Down
47 changes: 43 additions & 4 deletions app/service/FlowUpUserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,76 @@
import com.feth.play.module.pa.service.AbstractUserService;
import com.feth.play.module.pa.user.AuthUser;
import com.feth.play.module.pa.user.AuthUserIdentity;
import datasources.grafana.GrafanaClient;
import models.Organization;
import models.User;
import play.Logger;

import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.concurrent.ExecutionException;

@Singleton
public class FlowUpUserService extends AbstractUserService {

private final GrafanaClient grafanaClient;

@Inject
public FlowUpUserService(final PlayAuthenticate auth) {
public FlowUpUserService(PlayAuthenticate auth, GrafanaClient grafanaClient) {
super(auth);
this.grafanaClient = grafanaClient;
}

@Override
public Object save(final AuthUser authUser) {
public Object save(AuthUser authUser) {
final boolean isLinked = User.existsByAuthUserIdentity(authUser);
if (!isLinked) {
return User.create(authUser).id;
User user = User.create(authUser);
createGrafanaUser(user);
addUserToGrafanaOrg(user);
return user.getId();
} else {
User user = User.findByAuthUserIdentity(authUser);
createGrafanaUser(user);
addUserToGrafanaOrg(user);
// we have this user already, so return null
return null;
}
}

private void createGrafanaUser(User user) {
try {
grafanaClient.createUser(user).toCompletableFuture().get();
} catch (InterruptedException e) {
Logger.debug(e.getMessage());
} catch (ExecutionException e) {
Logger.debug(e.getMessage());
}
}

private void addUserToGrafanaOrg(User user) {
String[] split = user.getEmail().split("@");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If our user has a gmail email is going to share the org with other gmail users?

Copy link
Member Author

@davideme davideme Oct 25, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, domain should be set in Org configuration.
We will only allow domain, that are not @gmail.com or @googlemail.com

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's for a future PR.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, but if we only allow domains that are not @gmail.com how is a regular user w//o working at any organization going to test the platform?

if (split.length == 2) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you give semantic to this 2 with a constant?

Organization organization = Organization.findByGoogleAccount("@" + split[1]);
try {
grafanaClient.addUserToOrganisation(user, organization).toCompletableFuture().get();
user.refresh();
grafanaClient.deleteUserInDefaultOrganisation(user).toCompletableFuture().get();
} catch (InterruptedException e) {
Logger.debug(e.getMessage());
} catch (ExecutionException e) {
Logger.debug(e.getMessage());
}
}
}

@Override
public Object getLocalIdentity(final AuthUserIdentity identity) {
// For production: Caching might be a good idea here...
// ...and dont forget to sync the cache when users get deactivated/deleted
final User u = User.findByAuthUserIdentity(identity);
if(u != null) {
return u.id;
return u.getId();
} else {
return null;
}
Expand Down
18 changes: 5 additions & 13 deletions app/views/admin/user/list.scala.html
Original file line number Diff line number Diff line change
Expand Up @@ -78,27 +78,19 @@ <h1 id="homeTitle">@Messages("users.list.title", currentPage.getTotalRowCount)</

@for(user <- currentPage.getList) {
<tr>
<td><a href="@controllers.admin.routes.UserController.edit(user.id.toString)">@user.name</a></td>
<td><a href="@controllers.admin.routes.UserController.edit(user.getId.toString)">@user.getName</a></td>
<td>
@if(user.email == null) {
@if(user.getEmail == null) {
<em>-</em>
} else {
@user.email
@user.getEmail
}
</td>
<td>
@if(user.active == null) {
<em>-</em>
} else {
@user.active
}
@user.isActive
</td>
<td>
@if(user.emailValidated == null) {
<em>-</em>
} else {
@user.emailValidated
}
@user.isEmailValidated
</td>
</tr>
}
Expand Down
Loading