mirror of
https://github.com/apricote/home-cloud.git
synced 2026-01-13 13:01:03 +00:00
bitwarden deployment
This commit is contained in:
commit
42ec743a00
24 changed files with 498 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
keys/id_terraform*
|
||||||
|
credentials.tfvars
|
||||||
|
terraform.tfstate*
|
||||||
|
.terraform
|
||||||
25
Makefile
Normal file
25
Makefile
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
TF=terraform
|
||||||
|
TFFLAGS=-var-file=credentials.tfvars
|
||||||
|
VALIDATE=terraform validate -check-variables=false
|
||||||
|
|
||||||
|
apply: init
|
||||||
|
$(TF) apply $(TFFLAGS)
|
||||||
|
|
||||||
|
plan: init
|
||||||
|
$(TF) plan $(TFFLAGS)
|
||||||
|
|
||||||
|
destroy: init
|
||||||
|
$(TF) destroy $(TFFLAGS)
|
||||||
|
|
||||||
|
lint: init
|
||||||
|
$(VALIDATE) modules/docker_node
|
||||||
|
$(VALIDATE) modules/floating_ip
|
||||||
|
$(VALIDATE) services/bitwarden
|
||||||
|
$(VALIDATE) .
|
||||||
|
|
||||||
|
init: keys
|
||||||
|
$(TF) init
|
||||||
|
|
||||||
|
keys: keys/id_terraform
|
||||||
|
echo "No private key found! Generating Terraform SSH Keys."
|
||||||
|
./bootstrap-keys.sh
|
||||||
2
bootstrap-keys.sh
Executable file
2
bootstrap-keys.sh
Executable file
|
|
@ -0,0 +1,2 @@
|
||||||
|
ssh-keygen -t rsa -C "terraform@narando.de" -f keys/id_terraform
|
||||||
|
chmod 600 keys/id_terraform*
|
||||||
1
keys/README
Executable file
1
keys/README
Executable file
|
|
@ -0,0 +1 @@
|
||||||
|
Please execute ./bootstrap-keys.sh to generate TLS key for Terraform.
|
||||||
11
main.tf
Executable file
11
main.tf
Executable file
|
|
@ -0,0 +1,11 @@
|
||||||
|
module bitwarden {
|
||||||
|
source = "services/bitwarden"
|
||||||
|
|
||||||
|
location = "${var.hcloud_location}"
|
||||||
|
ssh_key_id = "${hcloud_ssh_key.terraform.id}"
|
||||||
|
bitwarden_admin_email = "${var.admin_email}"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable admin_email {
|
||||||
|
type = "string"
|
||||||
|
}
|
||||||
20
modules/docker_node/main.tf
Executable file
20
modules/docker_node/main.tf
Executable file
|
|
@ -0,0 +1,20 @@
|
||||||
|
resource "hcloud_server" "node" {
|
||||||
|
name = "${var.name}"
|
||||||
|
image = "${var.image}"
|
||||||
|
server_type = "${var.server_type}"
|
||||||
|
location = "${var.location}"
|
||||||
|
|
||||||
|
ssh_keys = ["${var.ssh_key_id}"]
|
||||||
|
|
||||||
|
connection {
|
||||||
|
private_key = "${file("./keys/id_terraform")}"
|
||||||
|
}
|
||||||
|
|
||||||
|
provisioner "remote-exec" {
|
||||||
|
scripts = [
|
||||||
|
"modules/docker_node/scripts/wait-cloud-init.sh",
|
||||||
|
"modules/docker_node/scripts/install-docker.sh",
|
||||||
|
"modules/docker_node/scripts/install-docker-compose.sh",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
7
modules/docker_node/outputs.tf
Executable file
7
modules/docker_node/outputs.tf
Executable file
|
|
@ -0,0 +1,7 @@
|
||||||
|
output ip {
|
||||||
|
value = "${hcloud_server.node.ipv4_address}"
|
||||||
|
}
|
||||||
|
|
||||||
|
output id {
|
||||||
|
value = "${hcloud_server.node.id}"
|
||||||
|
}
|
||||||
3
modules/docker_node/scripts/install-docker-compose.sh
Executable file
3
modules/docker_node/scripts/install-docker-compose.sh
Executable file
|
|
@ -0,0 +1,3 @@
|
||||||
|
curl -L "https://github.com/docker/compose/releases/download/1.23.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||||
|
sudo chmod +x /usr/local/bin/docker-compose
|
||||||
|
docker-compose --version
|
||||||
28
modules/docker_node/scripts/install-docker.sh
Executable file
28
modules/docker_node/scripts/install-docker.sh
Executable file
|
|
@ -0,0 +1,28 @@
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
# Source: https://docs.docker.com/install/linux/docker-ce/ubuntu/#install-using-the-repository
|
||||||
|
|
||||||
|
echo "# apt-get update"
|
||||||
|
apt-get update
|
||||||
|
echo "# apt-get upgrade -y"
|
||||||
|
DEBIAN_FRONTEND='noninteractive' apt-get -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' upgrade
|
||||||
|
|
||||||
|
# Add Repository
|
||||||
|
echo "# apt-get install"
|
||||||
|
apt-get install -y \
|
||||||
|
apt-transport-https \
|
||||||
|
ca-certificates \
|
||||||
|
curl \
|
||||||
|
software-properties-common
|
||||||
|
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add -
|
||||||
|
add-apt-repository \
|
||||||
|
"deb [arch=amd64] https://download.docker.com/linux/ubuntu \
|
||||||
|
$(lsb_release -cs) \
|
||||||
|
stable"
|
||||||
|
|
||||||
|
echo "# apt-get update"
|
||||||
|
apt-get update
|
||||||
|
|
||||||
|
# Install Docker
|
||||||
|
echo "# apt-get install docker-ce"
|
||||||
|
apt-get install -y docker-ce
|
||||||
13
modules/docker_node/scripts/wait-cloud-init.sh
Executable file
13
modules/docker_node/scripts/wait-cloud-init.sh
Executable file
|
|
@ -0,0 +1,13 @@
|
||||||
|
# cloud-init is running at boot time and blocking access to apt.
|
||||||
|
# Before doing anything we should wait for it to finish.
|
||||||
|
# cloud-init creates a file after finishing boot.
|
||||||
|
|
||||||
|
echo "Waiting for cloud-init to finish provisioning the instance."
|
||||||
|
while [ ! -f /var/lib/cloud/instance/boot-finished ]
|
||||||
|
do
|
||||||
|
echo "#"
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
# Wait some more to be sure
|
||||||
|
sleep 10
|
||||||
22
modules/docker_node/vars.tf
Executable file
22
modules/docker_node/vars.tf
Executable file
|
|
@ -0,0 +1,22 @@
|
||||||
|
variable name {
|
||||||
|
type = "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable image {
|
||||||
|
type = "string"
|
||||||
|
default = "ubuntu-18.04"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable server_type {
|
||||||
|
type = "string"
|
||||||
|
default = "cx11"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable location {
|
||||||
|
type = "string"
|
||||||
|
default = "nbg1"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable ssh_key_id {
|
||||||
|
type = "string"
|
||||||
|
}
|
||||||
4
modules/floating_ip/files/99-floating.cfg
Executable file
4
modules/floating_ip/files/99-floating.cfg
Executable file
|
|
@ -0,0 +1,4 @@
|
||||||
|
auto eth0:1
|
||||||
|
iface eth0:1 inet static
|
||||||
|
address ${FLOATING_IP}
|
||||||
|
netmask 255.255.255.255
|
||||||
48
modules/floating_ip/main.tf
Executable file
48
modules/floating_ip/main.tf
Executable file
|
|
@ -0,0 +1,48 @@
|
||||||
|
#################
|
||||||
|
### IP ADDRESS ##
|
||||||
|
#################
|
||||||
|
|
||||||
|
resource hcloud_floating_ip main {
|
||||||
|
type = "${var.type}"
|
||||||
|
description = "${var.host}"
|
||||||
|
home_location = "${var.location}"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "hcloud_rdns" "main" {
|
||||||
|
floating_ip_id = "${hcloud_floating_ip.main.id}"
|
||||||
|
ip_address = "${hcloud_floating_ip.main.ip_address}"
|
||||||
|
dns_ptr = "${var.host}"
|
||||||
|
}
|
||||||
|
|
||||||
|
###################################
|
||||||
|
### ASSIGNMENT AND PROVISIONING ###
|
||||||
|
###################################
|
||||||
|
|
||||||
|
data "template_file" "network_config" {
|
||||||
|
template = "${file("modules/floating_ip/files/99-floating.cfg")}"
|
||||||
|
|
||||||
|
vars {
|
||||||
|
FLOATING_IP = "${hcloud_floating_ip.main.ip_address}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource hcloud_floating_ip_assignment main {
|
||||||
|
floating_ip_id = "${hcloud_floating_ip.main.id}"
|
||||||
|
server_id = "${var.server_id}"
|
||||||
|
|
||||||
|
connection = {
|
||||||
|
host = "${var.server_ip}"
|
||||||
|
private_key = "${file("keys/id_terraform")}"
|
||||||
|
}
|
||||||
|
|
||||||
|
provisioner file {
|
||||||
|
content = "${data.template_file.network_config.rendered}"
|
||||||
|
destination = "/etc/network/interfaces.d/99-floating.cfg"
|
||||||
|
}
|
||||||
|
|
||||||
|
provisioner remote-exec {
|
||||||
|
inline = [
|
||||||
|
"ifdown eth0:1 ; ifup eth0:1",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
3
modules/floating_ip/outputs.tf
Executable file
3
modules/floating_ip/outputs.tf
Executable file
|
|
@ -0,0 +1,3 @@
|
||||||
|
output ip {
|
||||||
|
value = "${hcloud_floating_ip.main.ip_address}"
|
||||||
|
}
|
||||||
21
modules/floating_ip/vars.tf
Executable file
21
modules/floating_ip/vars.tf
Executable file
|
|
@ -0,0 +1,21 @@
|
||||||
|
variable host {
|
||||||
|
type = "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable type {
|
||||||
|
type = "string"
|
||||||
|
default = "ipv4"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable server_id {
|
||||||
|
type = "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable server_ip {
|
||||||
|
type = "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable location {
|
||||||
|
type = "string"
|
||||||
|
default = "nbg1"
|
||||||
|
}
|
||||||
3
output.tf
Executable file
3
output.tf
Executable file
|
|
@ -0,0 +1,3 @@
|
||||||
|
output bitwarden_ip {
|
||||||
|
value = "${module.bitwarden.ip}"
|
||||||
|
}
|
||||||
25
provider_hcloud.tf
Executable file
25
provider_hcloud.tf
Executable file
|
|
@ -0,0 +1,25 @@
|
||||||
|
# Set the variable value in *.tfvars file
|
||||||
|
# or using -var="hcloud_token=..." CLI option
|
||||||
|
variable "hcloud_token" {}
|
||||||
|
|
||||||
|
variable "hcloud_location" {}
|
||||||
|
|
||||||
|
# Configure the Hetzner Cloud Provider
|
||||||
|
provider "hcloud" {
|
||||||
|
version = "~> 1.7.0"
|
||||||
|
|
||||||
|
token = "${var.hcloud_token}"
|
||||||
|
}
|
||||||
|
|
||||||
|
#######################
|
||||||
|
## Terraform SSH Key ##
|
||||||
|
#######################
|
||||||
|
|
||||||
|
resource "hcloud_ssh_key" "terraform" {
|
||||||
|
name = "terraform"
|
||||||
|
public_key = "${file("./keys/id_terraform.pub")}"
|
||||||
|
|
||||||
|
labels = {
|
||||||
|
"description" = "Used by terraform to provision nodes"
|
||||||
|
}
|
||||||
|
}
|
||||||
7
provider_other.tf
Executable file
7
provider_other.tf
Executable file
|
|
@ -0,0 +1,7 @@
|
||||||
|
provider "null" {
|
||||||
|
version = "~> 1.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
provider "template" {
|
||||||
|
version = "~> 1.0"
|
||||||
|
}
|
||||||
46
services/bitwarden/files/docker-compose.yaml
Normal file
46
services/bitwarden/files/docker-compose.yaml
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
version: "2.1"
|
||||||
|
|
||||||
|
services:
|
||||||
|
traefik:
|
||||||
|
image: traefik:1.7
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- 80:80
|
||||||
|
- 443:443
|
||||||
|
networks:
|
||||||
|
- web
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
- ${INSTALL_DIR}/traefik.toml:/traefik.toml
|
||||||
|
- ${INSTALL_DIR}/acme.json:/acme.json
|
||||||
|
container_name: traefik
|
||||||
|
|
||||||
|
bitwarden:
|
||||||
|
image: mprasil/bitwarden:latest
|
||||||
|
restart: always
|
||||||
|
expose:
|
||||||
|
- "80"
|
||||||
|
- "3012"
|
||||||
|
networks:
|
||||||
|
- web
|
||||||
|
volumes:
|
||||||
|
- ${BITWARDEN_DATA_DIR}/:/data/
|
||||||
|
environment:
|
||||||
|
SIGNUPS_ALLOWED: "false"
|
||||||
|
SERVER_ADMIN_EMAIL: "${BITWARDEN_ADMIN_EMAIL}"
|
||||||
|
labels:
|
||||||
|
- "traefik.frontend.rule=Host:${HOST}"
|
||||||
|
- "traefik.docker.network=web"
|
||||||
|
- "traefik.port=80"
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.web.frontend.rule=Host:${HOST}"
|
||||||
|
- "traefik.web.port=80"
|
||||||
|
- "traefik.hub.frontend.rule=Path:/notifications/hub"
|
||||||
|
- "traefik.hub.port=3012"
|
||||||
|
- "traefik.negotiate.frontend.rule=Path:/notifications/hub/negotiate"
|
||||||
|
- "traefik.negotiate.port=80"
|
||||||
|
container_name: bitwarden
|
||||||
|
|
||||||
|
networks:
|
||||||
|
web:
|
||||||
|
name: web
|
||||||
28
services/bitwarden/files/traefik.toml
Normal file
28
services/bitwarden/files/traefik.toml
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
debug = true
|
||||||
|
|
||||||
|
logLevel = "INFO"
|
||||||
|
defaultEntryPoints = ["https","http"]
|
||||||
|
|
||||||
|
[entryPoints]
|
||||||
|
[entryPoints.http]
|
||||||
|
address = ":80"
|
||||||
|
[entryPoints.http.redirect]
|
||||||
|
entryPoint = "https"
|
||||||
|
[entryPoints.https]
|
||||||
|
address = ":443"
|
||||||
|
[entryPoints.https.tls]
|
||||||
|
|
||||||
|
[retry]
|
||||||
|
|
||||||
|
[docker]
|
||||||
|
endpoint = "unix:///var/run/docker.sock"
|
||||||
|
watch = true
|
||||||
|
exposedByDefault = false
|
||||||
|
|
||||||
|
[acme]
|
||||||
|
email = "julian.toelle97@gmail.com"
|
||||||
|
storage = "acme.json"
|
||||||
|
entryPoint = "https"
|
||||||
|
onHostRule = true
|
||||||
|
[acme.httpChallenge]
|
||||||
|
entryPoint = "http"
|
||||||
134
services/bitwarden/main.tf
Executable file
134
services/bitwarden/main.tf
Executable file
|
|
@ -0,0 +1,134 @@
|
||||||
|
##########
|
||||||
|
## NODE ##
|
||||||
|
##########
|
||||||
|
module "node" {
|
||||||
|
source = "../../modules/docker_node"
|
||||||
|
|
||||||
|
name = "${var.name}"
|
||||||
|
|
||||||
|
ssh_key_id = "${var.ssh_key_id}"
|
||||||
|
}
|
||||||
|
|
||||||
|
############
|
||||||
|
## VOLUME ##
|
||||||
|
############
|
||||||
|
|
||||||
|
resource "hcloud_volume" "data" {
|
||||||
|
name = "${var.volume_name}"
|
||||||
|
size = "${var.volume_size}"
|
||||||
|
location = "${var.location}"
|
||||||
|
|
||||||
|
format = "ext4"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource hcloud_volume_attachment "data" {
|
||||||
|
volume_id = "${hcloud_volume.data.id}"
|
||||||
|
server_id = "${module.node.id}"
|
||||||
|
|
||||||
|
automount = true
|
||||||
|
}
|
||||||
|
|
||||||
|
resource null_resource "start-stop-bitwarden" {
|
||||||
|
# This resource is responsible for starting and stopping Bitwarden before
|
||||||
|
# changing volume assignments. This should avoid data corruption.
|
||||||
|
depends_on = ["hcloud_volume_attachment.data", "null_resource.install-bitwarden"]
|
||||||
|
|
||||||
|
triggers = {
|
||||||
|
id = "${hcloud_volume_attachment.data.id}"
|
||||||
|
}
|
||||||
|
|
||||||
|
connection = {
|
||||||
|
host = "${module.node.ip}"
|
||||||
|
private_key = "${file("keys/id_terraform")}"
|
||||||
|
}
|
||||||
|
|
||||||
|
provisioner remote-exec {
|
||||||
|
# Stop bitwarden container before unmounting data volume
|
||||||
|
when = "destroy"
|
||||||
|
|
||||||
|
inline = [
|
||||||
|
"echo Stopping Bitwarden",
|
||||||
|
"docker stop bitwarden",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
provisioner remote-exec {
|
||||||
|
# Start bitwarden after mounting new volume
|
||||||
|
inline = [
|
||||||
|
"echo Starting Bitwarden",
|
||||||
|
"cd ${local.install_dir}",
|
||||||
|
"docker-compose up -d",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
################
|
||||||
|
## IP ADDRESS ##
|
||||||
|
################
|
||||||
|
|
||||||
|
module floating_ip {
|
||||||
|
source = "../../modules/floating_ip"
|
||||||
|
|
||||||
|
location = "${var.location}"
|
||||||
|
host = "${var.host}"
|
||||||
|
server_id = "${module.node.id}"
|
||||||
|
server_ip = "${module.node.ip}"
|
||||||
|
}
|
||||||
|
|
||||||
|
#################
|
||||||
|
## APPLICATION ##
|
||||||
|
#################
|
||||||
|
|
||||||
|
data "template_file" "compose" {
|
||||||
|
template = "${file("services/bitwarden/files/docker-compose.yaml")}"
|
||||||
|
|
||||||
|
vars = {
|
||||||
|
INSTALL_DIR = "${local.install_dir}"
|
||||||
|
BITWARDEN_DATA_DIR = "${local.bitwarden_data_dir}"
|
||||||
|
BITWARDEN_ADMIN_EMAIL = "${var.bitwarden_admin_email}"
|
||||||
|
HOST = "${var.host}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "null_resource" "install-bitwarden" {
|
||||||
|
depends_on = ["module.node", "hcloud_volume_attachment.data"]
|
||||||
|
|
||||||
|
triggers {
|
||||||
|
node_id = "${module.node.id}"
|
||||||
|
volume_id = "${hcloud_volume.data.id}"
|
||||||
|
|
||||||
|
docker_compose = "${sha1(data.template_file.compose.rendered)}"
|
||||||
|
traefik_config = "${sha1(file("services/bitwarden/files/traefik.toml"))}"
|
||||||
|
}
|
||||||
|
|
||||||
|
connection = {
|
||||||
|
host = "${module.node.ip}"
|
||||||
|
private_key = "${file("keys/id_terraform")}"
|
||||||
|
}
|
||||||
|
|
||||||
|
provisioner remote-exec {
|
||||||
|
inline = [
|
||||||
|
"mkdir -p ${local.install_dir}",
|
||||||
|
"touch ${local.install_dir}/acme.json",
|
||||||
|
"chmod 600 ${local.install_dir}/acme.json",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
provisioner file {
|
||||||
|
content = "${data.template_file.compose.rendered}"
|
||||||
|
destination = "${local.install_dir}/docker-compose.yaml"
|
||||||
|
}
|
||||||
|
|
||||||
|
provisioner file {
|
||||||
|
source = "services/bitwarden/files/traefik.toml"
|
||||||
|
destination = "${local.install_dir}/traefik.toml"
|
||||||
|
}
|
||||||
|
|
||||||
|
provisioner remote-exec {
|
||||||
|
inline = [
|
||||||
|
"cd ${local.install_dir}",
|
||||||
|
"docker-compose pull",
|
||||||
|
"docker-compose up -d",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
3
services/bitwarden/output.tf
Executable file
3
services/bitwarden/output.tf
Executable file
|
|
@ -0,0 +1,3 @@
|
||||||
|
output ip {
|
||||||
|
value = "${module.floating_ip.ip}"
|
||||||
|
}
|
||||||
39
services/bitwarden/vars.tf
Executable file
39
services/bitwarden/vars.tf
Executable file
|
|
@ -0,0 +1,39 @@
|
||||||
|
variable location {
|
||||||
|
type = "string"
|
||||||
|
default = "nbg1"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable ssh_key_id {
|
||||||
|
type = "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable volume_size {
|
||||||
|
type = "string"
|
||||||
|
default = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
variable name {
|
||||||
|
type = "string"
|
||||||
|
default = "bitwarden"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable volume_name {
|
||||||
|
type = "string"
|
||||||
|
default = "bitwarden-data"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable host {
|
||||||
|
type = "string"
|
||||||
|
default = "bitwarden.apricote.de"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable bitwarden_admin_email {
|
||||||
|
type = "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
locals = {
|
||||||
|
volume_path = "/mnt/${var.volume_name}"
|
||||||
|
|
||||||
|
install_dir = "/opt/${var.name}"
|
||||||
|
bitwarden_data_dir = "${local.volume_path}"
|
||||||
|
}
|
||||||
1
terraform.tfvars
Executable file
1
terraform.tfvars
Executable file
|
|
@ -0,0 +1 @@
|
||||||
|
hcloud_location = "nbg1"
|
||||||
Loading…
Add table
Add a link
Reference in a new issue