Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5689d75a87 | ||
|
|
1325b6750e | ||
|
|
638005403f | ||
|
|
55f274532f | ||
|
|
66f76c2036 | ||
|
|
865cabb525 | ||
|
|
7c3c4bc58f | ||
|
|
2dbe8c733b | ||
|
|
84352b82cc | ||
|
|
383c1f87af | ||
|
|
ce3003b026 | ||
|
|
e06f4b2f9e | ||
|
|
b721fd894e | ||
|
|
b8ea6fd4b3 | ||
|
|
dbda63263b | ||
|
|
93cdc31be0 | ||
|
|
4dfbe46690 | ||
|
|
0033f9caeb | ||
|
|
74839800a9 | ||
|
|
b75337e21e | ||
|
|
d94f941189 | ||
|
|
0b34f067cb | ||
|
|
dbe51832b0 | ||
|
|
3a2a53be95 | ||
|
|
b2bf95479e | ||
|
|
48f764e347 | ||
|
|
de62d0655e | ||
|
|
38aecff886 | ||
|
|
e4a3215257 | ||
|
|
c705b5fbef | ||
|
|
69216b4c5e | ||
|
|
5d2930745a | ||
|
|
8fc3d1ef24 | ||
|
|
d33e4fb0ed | ||
|
|
1217ef1b56 | ||
|
|
c9943d70ec | ||
|
|
08dca57eb4 |
@@ -1,3 +1,19 @@
|
|||||||
|
## 0.43.0 (2012-04-20)
|
||||||
|
|
||||||
|
* wrap supervisord env vars in quotes [Raphael Randschau]
|
||||||
|
|
||||||
|
## 0.42.0 (2012-04-18)
|
||||||
|
|
||||||
|
* Move read_environment to a public class method. [Phil Hagelberg]
|
||||||
|
* Drop parka dependency [Phil Hagelberg]
|
||||||
|
* add group support for supervisord [Raphael Randschau]
|
||||||
|
* fix enviroment export [Raphael Randschau]
|
||||||
|
|
||||||
|
## 0.41.0 (2012-03-16)
|
||||||
|
|
||||||
|
* replace term-ansicolor with built-in colorization [David Dollar]
|
||||||
|
* supervisord export template [Raphael Randschau]
|
||||||
|
|
||||||
## 0.40.0 (2012-02-24)
|
## 0.40.0 (2012-02-24)
|
||||||
|
|
||||||
* support various quoting styles in .env [David Dollar]
|
* support various quoting styles in .env [David Dollar]
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ end
|
|||||||
|
|
||||||
group :development do
|
group :development do
|
||||||
gem 'aws-s3'
|
gem 'aws-s3'
|
||||||
gem 'parka'
|
|
||||||
gem 'rake'
|
gem 'rake'
|
||||||
gem 'ronn'
|
gem 'ronn'
|
||||||
gem 'fakefs', '~> 0.3.2'
|
gem 'fakefs', '~> 0.3.2'
|
||||||
|
|||||||
+1
-9
@@ -1,7 +1,7 @@
|
|||||||
PATH
|
PATH
|
||||||
remote: .
|
remote: .
|
||||||
specs:
|
specs:
|
||||||
foreman (0.41.0)
|
foreman (0.44.0)
|
||||||
thor (>= 0.13.6)
|
thor (>= 0.13.6)
|
||||||
|
|
||||||
GEM
|
GEM
|
||||||
@@ -12,7 +12,6 @@ GEM
|
|||||||
mime-types
|
mime-types
|
||||||
xml-simple
|
xml-simple
|
||||||
builder (3.0.0)
|
builder (3.0.0)
|
||||||
crack (0.1.8)
|
|
||||||
diff-lcs (1.1.3)
|
diff-lcs (1.1.3)
|
||||||
fakefs (0.3.2)
|
fakefs (0.3.2)
|
||||||
hpricot (0.8.6)
|
hpricot (0.8.6)
|
||||||
@@ -20,15 +19,9 @@ GEM
|
|||||||
mime-types (1.16)
|
mime-types (1.16)
|
||||||
multi_json (1.0.4)
|
multi_json (1.0.4)
|
||||||
mustache (0.11.2)
|
mustache (0.11.2)
|
||||||
parka (0.6.2)
|
|
||||||
crack
|
|
||||||
rest-client
|
|
||||||
thor
|
|
||||||
posix-spawn (0.3.6)
|
posix-spawn (0.3.6)
|
||||||
rake (0.9.2.2)
|
rake (0.9.2.2)
|
||||||
rdiscount (1.6.5)
|
rdiscount (1.6.5)
|
||||||
rest-client (1.6.1)
|
|
||||||
mime-types (>= 1.16)
|
|
||||||
ronn (0.7.3)
|
ronn (0.7.3)
|
||||||
hpricot (>= 0.8.2)
|
hpricot (>= 0.8.2)
|
||||||
mustache (>= 0.7.0)
|
mustache (>= 0.7.0)
|
||||||
@@ -59,7 +52,6 @@ DEPENDENCIES
|
|||||||
aws-s3
|
aws-s3
|
||||||
fakefs (~> 0.3.2)
|
fakefs (~> 0.3.2)
|
||||||
foreman!
|
foreman!
|
||||||
parka
|
|
||||||
posix-spawn (~> 0.3.6)
|
posix-spawn (~> 0.3.6)
|
||||||
rake
|
rake
|
||||||
ronn
|
ronn
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
<%
|
<%
|
||||||
|
app_names = []
|
||||||
engine.procfile.entries.each do |process|
|
engine.procfile.entries.each do |process|
|
||||||
next if (conc = self.concurrency[process.name]) < 1
|
next if (conc = self.concurrency[process.name]) < 1
|
||||||
1.upto(self.concurrency[process.name]) do |num|
|
1.upto(self.concurrency[process.name]) do |num|
|
||||||
port = engine.port_for(process, num, self.port)
|
port = engine.port_for(process, num, self.port)
|
||||||
name = if (conc > 1); "#{process.name}-#{num}" else process.name; end
|
name = if (conc > 1); "#{process.name}-#{num}" else process.name; end
|
||||||
environment = (engine.environment.each_pair { |var,env| "#{var.upcase}=#{env}" }.to_a << "PORT=#{port}")
|
environment = (engine.environment.keys.sort.map{ |var| %{#{var.upcase}="#{engine.environment[var]}"} } + [%{PORT="#{port}"}])
|
||||||
|
app_name = "#{app}-#{name}"
|
||||||
|
app_names << app_name
|
||||||
%>
|
%>
|
||||||
[program:<%= app %>-<%= name %>]
|
[program:<%= app_name %>]
|
||||||
command=<%= process.command %>
|
command=<%= process.command %>
|
||||||
autostart=true
|
autostart=true
|
||||||
autorestart=true
|
autorestart=true
|
||||||
@@ -18,4 +21,7 @@ directory=<%= engine.directory %>
|
|||||||
environment=<%= environment.join(',') %><%
|
environment=<%= environment.join(',') %><%
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
%>
|
%>
|
||||||
|
|
||||||
|
[group:<%= app %>]
|
||||||
|
programs=<%= app_names.join(',') %>
|
||||||
|
|||||||
+5
-1
@@ -83,7 +83,11 @@ private ######################################################################
|
|||||||
end
|
end
|
||||||
|
|
||||||
def procfile
|
def procfile
|
||||||
options[:procfile] || "Procfile"
|
case
|
||||||
|
when options[:procfile] then options[:procfile]
|
||||||
|
when options[:app_root] then File.expand_path(File.join(options[:app_root], "Procfile"))
|
||||||
|
else "Procfile"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def error(message)
|
def error(message)
|
||||||
|
|||||||
+24
-17
@@ -24,8 +24,10 @@ class Foreman::Engine
|
|||||||
@procfile = Foreman::Procfile.new(procfile)
|
@procfile = Foreman::Procfile.new(procfile)
|
||||||
@directory = options[:app_root] || File.expand_path(File.dirname(procfile))
|
@directory = options[:app_root] || File.expand_path(File.dirname(procfile))
|
||||||
@options = options.dup
|
@options = options.dup
|
||||||
@environment = read_environment_files(options[:env])
|
|
||||||
@output_mutex = Mutex.new
|
@output_mutex = Mutex.new
|
||||||
|
|
||||||
|
@options[:env] ||= default_env
|
||||||
|
@environment = read_environment_files(@options[:env])
|
||||||
end
|
end
|
||||||
|
|
||||||
def start
|
def start
|
||||||
@@ -51,6 +53,22 @@ class Foreman::Engine
|
|||||||
environment.each { |k,v| ENV[k] = v }
|
environment.each { |k,v| ENV[k] = v }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.read_environment(filename)
|
||||||
|
return {} unless File.exists?(filename)
|
||||||
|
|
||||||
|
File.read(filename).split("\n").inject({}) do |hash, line|
|
||||||
|
if line =~ /\A([A-Za-z_0-9]+)=(.*)\z/
|
||||||
|
key, val = [$1, $2]
|
||||||
|
case val
|
||||||
|
when /\A'(.*)'\z/ then hash[key] = $1
|
||||||
|
when /\A"(.*)"\z/ then hash[key] = $1.gsub(/\\(.)/, '\1')
|
||||||
|
else hash[key] = val
|
||||||
|
end
|
||||||
|
end
|
||||||
|
hash
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private ######################################################################
|
private ######################################################################
|
||||||
|
|
||||||
def spawn_processes
|
def spawn_processes
|
||||||
@@ -195,26 +213,15 @@ private ######################################################################
|
|||||||
|
|
||||||
(filenames || "").split(",").map(&:strip).each do |filename|
|
(filenames || "").split(",").map(&:strip).each do |filename|
|
||||||
error "No such file: #{filename}" unless File.exists?(filename)
|
error "No such file: #{filename}" unless File.exists?(filename)
|
||||||
environment.merge!(read_environment(filename))
|
environment.merge!(Foreman::Engine.read_environment(filename))
|
||||||
end
|
end
|
||||||
|
|
||||||
environment.merge!(read_environment(".env")) unless filenames
|
|
||||||
environment
|
environment
|
||||||
end
|
end
|
||||||
|
|
||||||
def read_environment(filename)
|
def default_env
|
||||||
return {} unless File.exists?(filename)
|
env = File.join(directory, ".env")
|
||||||
|
File.exists?(env) ? env : ""
|
||||||
File.read(filename).split("\n").inject({}) do |hash, line|
|
|
||||||
if line =~ /\A([A-Za-z_0-9]+)=(.*)\z/
|
|
||||||
key, val = [$1, $2]
|
|
||||||
case val
|
|
||||||
when /\A'(.*)'\z/ then hash[key] = $1
|
|
||||||
when /\A"(.*)"\z/ then hash[key] = $1.gsub(/\\(.)/, '\1')
|
|
||||||
else hash[key] = val
|
|
||||||
end
|
|
||||||
end
|
|
||||||
hash
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
module Foreman
|
module Foreman
|
||||||
|
|
||||||
VERSION = "0.41.0"
|
VERSION = "0.44.0"
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|||||||
+9
-1
@@ -1,7 +1,7 @@
|
|||||||
.\" generated with Ronn/v0.7.3
|
.\" generated with Ronn/v0.7.3
|
||||||
.\" http://github.com/rtomayko/ronn/tree/0.7.3
|
.\" http://github.com/rtomayko/ronn/tree/0.7.3
|
||||||
.
|
.
|
||||||
.TH "FOREMAN" "1" "February 2012" "Foreman 0.39.0" "Foreman Manual"
|
.TH "FOREMAN" "1" "April 2012" "Foreman 0.43.0" "Foreman Manual"
|
||||||
.
|
.
|
||||||
.SH "NAME"
|
.SH "NAME"
|
||||||
\fBforeman\fR \- manage Procfile\-based applications
|
\fBforeman\fR \- manage Procfile\-based applications
|
||||||
@@ -35,6 +35,14 @@ The following options control how the application is run:
|
|||||||
Specify the number of each process type to run\. The value passed in should be in the format \fBprocess=num,process=num\fR
|
Specify the number of each process type to run\. The value passed in should be in the format \fBprocess=num,process=num\fR
|
||||||
.
|
.
|
||||||
.TP
|
.TP
|
||||||
|
\fB\-e\fR, \fB\-\-env\fR
|
||||||
|
Specify one or more \.env files to load
|
||||||
|
.
|
||||||
|
.TP
|
||||||
|
\fB\-f\fR, \fB\-\-procfile\fR
|
||||||
|
Specify an alternate Procfile to load, implies \fB\-d\fR at the Procfile root\.
|
||||||
|
.
|
||||||
|
.TP
|
||||||
\fB\-p\fR, \fB\-\-port\fR
|
\fB\-p\fR, \fB\-\-port\fR
|
||||||
Specify which port to use as the base for this application\. Should be a multiple of 1000\.
|
Specify which port to use as the base for this application\. Should be a multiple of 1000\.
|
||||||
.
|
.
|
||||||
|
|||||||
@@ -30,6 +30,12 @@ The following options control how the application is run:
|
|||||||
Specify the number of each process type to run. The value passed in
|
Specify the number of each process type to run. The value passed in
|
||||||
should be in the format `process=num,process=num`
|
should be in the format `process=num,process=num`
|
||||||
|
|
||||||
|
* `-e`, `--env`:
|
||||||
|
Specify one or more .env files to load
|
||||||
|
|
||||||
|
* `-f`, `--procfile`:
|
||||||
|
Specify an alternate Procfile to load, implies `-d` at the Procfile root.
|
||||||
|
|
||||||
* `-p`, `--port`:
|
* `-p`, `--port`:
|
||||||
Specify which port to use as the base for this application. Should be
|
Specify which port to use as the base for this application. Should be
|
||||||
a multiple of 1000.
|
a multiple of 1000.
|
||||||
|
|||||||
@@ -34,6 +34,15 @@ describe "Foreman::CLI", :fakefs do
|
|||||||
subject.start("alpha")
|
subject.start("alpha")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "with an alternate root" do
|
||||||
|
it "reads the Procfile from that root" do
|
||||||
|
write_procfile "/some/app/Procfile"
|
||||||
|
mock(Foreman::Procfile).new("/some/app/Procfile")
|
||||||
|
mock.instance_of(Foreman::Engine).start
|
||||||
|
foreman %{ start -d /some/app }
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "export" do
|
describe "export" do
|
||||||
|
|||||||
@@ -49,6 +49,14 @@ describe "Foreman::Engine", :fakefs do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "directories" do
|
||||||
|
it "has the directory default relative to the Procfile" do
|
||||||
|
write_procfile "/some/app/Procfile"
|
||||||
|
engine = Foreman::Engine.new("/some/app/Procfile")
|
||||||
|
engine.directory.should == "/some/app"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "environment" do
|
describe "environment" do
|
||||||
before(:each) do
|
before(:each) do
|
||||||
write_procfile
|
write_procfile
|
||||||
@@ -97,6 +105,15 @@ describe "Foreman::Engine", :fakefs do
|
|||||||
engine.environment.should == {"FOO"=>"qoo"}
|
engine.environment.should == {"FOO"=>"qoo"}
|
||||||
engine.start
|
engine.start
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "should be loaded relative to the Procfile" do
|
||||||
|
FileUtils.mkdir_p "/some/app"
|
||||||
|
File.open("/some/app/.env", "w") { |f| f.puts("FOO=qoo") }
|
||||||
|
write_procfile "/some/app/Procfile"
|
||||||
|
engine = Foreman::Engine.new("/some/app/Procfile")
|
||||||
|
engine.environment.should == {"FOO"=>"qoo"}
|
||||||
|
engine.start
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "utf8" do
|
describe "utf8" do
|
||||||
|
|||||||
@@ -15,12 +15,11 @@ describe Foreman::Export::Supervisord, :fakefs do
|
|||||||
it "exports to the filesystem" do
|
it "exports to the filesystem" do
|
||||||
supervisord.export
|
supervisord.export
|
||||||
|
|
||||||
File.read("/tmp/init/app.conf").should == example_export_file("supervisord/app.conf")
|
File.read("/tmp/init/app.conf").should == example_export_file("supervisord/app.conf")
|
||||||
end
|
end
|
||||||
|
|
||||||
it "cleans up if exporting into an existing dir" do
|
it "cleans up if exporting into an existing dir" do
|
||||||
mock(FileUtils).rm("/tmp/init/app.conf")
|
mock(FileUtils).rm("/tmp/init/app.conf")
|
||||||
|
|
||||||
supervisord.export
|
supervisord.export
|
||||||
supervisord.export
|
supervisord.export
|
||||||
end
|
end
|
||||||
@@ -30,8 +29,7 @@ describe Foreman::Export::Supervisord, :fakefs do
|
|||||||
|
|
||||||
it "exports to the filesystem with concurrency" do
|
it "exports to the filesystem with concurrency" do
|
||||||
supervisord.export
|
supervisord.export
|
||||||
|
File.read("/tmp/init/app.conf").should == example_export_file("supervisord/app-alpha-2.conf")
|
||||||
File.read("/tmp/init/app.conf").should == example_export_file("supervisord/app-alpha-2.conf")
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -46,7 +44,6 @@ describe Foreman::Export::Supervisord, :fakefs do
|
|||||||
|
|
||||||
it "can export with alternate template files" do
|
it "can export with alternate template files" do
|
||||||
supervisord.export
|
supervisord.export
|
||||||
|
|
||||||
File.read("/tmp/init/app.conf").should == "alternate_template\n"
|
File.read("/tmp/init/app.conf").should == "alternate_template\n"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -67,9 +64,22 @@ describe Foreman::Export::Supervisord, :fakefs do
|
|||||||
|
|
||||||
it "can export with alternate template files" do
|
it "can export with alternate template files" do
|
||||||
supervisord.export
|
supervisord.export
|
||||||
|
|
||||||
File.read("/tmp/init/app.conf").should == "default_alternate_template\n"
|
File.read("/tmp/init/app.conf").should == "default_alternate_template\n"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "environment export" do
|
||||||
|
it "correctly translates environment when exporting" do
|
||||||
|
File.open("/tmp/supervisord_env", "w") { |f| f.puts("QUEUE=fastqueue,slowqueue\nVERBOSE=1") }
|
||||||
|
|
||||||
|
engine = Foreman::Engine.new(procfile,:env => "/tmp/supervisord_env")
|
||||||
|
supervisor = Foreman::Export::Supervisord.new("/tmp/init", engine, options)
|
||||||
|
stub(supervisor).say
|
||||||
|
|
||||||
|
supervisor.export
|
||||||
|
|
||||||
|
File.read("/tmp/init/app.conf").should == example_export_file("supervisord/app-env-with-comma.conf")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ stdout_logfile=/var/log/app/alpha-1-out.log
|
|||||||
stderr_logfile=/var/log/app/alpha-1-err.log
|
stderr_logfile=/var/log/app/alpha-1-err.log
|
||||||
user=app
|
user=app
|
||||||
directory=/tmp/app
|
directory=/tmp/app
|
||||||
environment=PORT=5000
|
environment=PORT="5000"
|
||||||
[program:app-alpha-2]
|
[program:app-alpha-2]
|
||||||
command=./alpha
|
command=./alpha
|
||||||
autostart=true
|
autostart=true
|
||||||
@@ -18,4 +18,7 @@ stdout_logfile=/var/log/app/alpha-2-out.log
|
|||||||
stderr_logfile=/var/log/app/alpha-2-err.log
|
stderr_logfile=/var/log/app/alpha-2-err.log
|
||||||
user=app
|
user=app
|
||||||
directory=/tmp/app
|
directory=/tmp/app
|
||||||
environment=PORT=5001
|
environment=PORT="5001"
|
||||||
|
|
||||||
|
[group:app]
|
||||||
|
programs=app-alpha-1,app-alpha-2
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
|
||||||
|
[program:app-alpha]
|
||||||
|
command=./alpha
|
||||||
|
autostart=true
|
||||||
|
autorestart=true
|
||||||
|
stopsignal=QUIT
|
||||||
|
stdout_logfile=/var/log/app/alpha-1-out.log
|
||||||
|
stderr_logfile=/var/log/app/alpha-1-err.log
|
||||||
|
user=app
|
||||||
|
directory=/tmp/app
|
||||||
|
environment=QUEUE="fastqueue,slowqueue",VERBOSE="1",PORT="5000"
|
||||||
|
[program:app-bravo]
|
||||||
|
command=./bravo
|
||||||
|
autostart=true
|
||||||
|
autorestart=true
|
||||||
|
stopsignal=QUIT
|
||||||
|
stdout_logfile=/var/log/app/bravo-1-out.log
|
||||||
|
stderr_logfile=/var/log/app/bravo-1-err.log
|
||||||
|
user=app
|
||||||
|
directory=/tmp/app
|
||||||
|
environment=QUEUE="fastqueue,slowqueue",VERBOSE="1",PORT="5100"
|
||||||
|
|
||||||
|
[group:app]
|
||||||
|
programs=app-alpha,app-bravo
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
|
||||||
|
[program:app-alpha]
|
||||||
|
command=./alpha
|
||||||
|
autostart=true
|
||||||
|
autorestart=true
|
||||||
|
stopsignal=QUIT
|
||||||
|
stdout_logfile=/var/log/app/alpha-1-out.log
|
||||||
|
stderr_logfile=/var/log/app/alpha-1-err.log
|
||||||
|
user=app
|
||||||
|
directory=/tmp/app
|
||||||
|
environment=FOO="bar",PORT="5000"
|
||||||
|
[program:app-bravo]
|
||||||
|
command=./bravo
|
||||||
|
autostart=true
|
||||||
|
autorestart=true
|
||||||
|
stopsignal=QUIT
|
||||||
|
stdout_logfile=/var/log/app/bravo-1-out.log
|
||||||
|
stderr_logfile=/var/log/app/bravo-1-err.log
|
||||||
|
user=app
|
||||||
|
directory=/tmp/app
|
||||||
|
environment=FOO="bar",PORT="5100"
|
||||||
@@ -8,7 +8,7 @@ stdout_logfile=/var/log/app/alpha-1-out.log
|
|||||||
stderr_logfile=/var/log/app/alpha-1-err.log
|
stderr_logfile=/var/log/app/alpha-1-err.log
|
||||||
user=app
|
user=app
|
||||||
directory=/tmp/app
|
directory=/tmp/app
|
||||||
environment=PORT=5000
|
environment=PORT="5000"
|
||||||
[program:app-bravo]
|
[program:app-bravo]
|
||||||
command=./bravo
|
command=./bravo
|
||||||
autostart=true
|
autostart=true
|
||||||
@@ -18,4 +18,7 @@ stdout_logfile=/var/log/app/bravo-1-out.log
|
|||||||
stderr_logfile=/var/log/app/bravo-1-err.log
|
stderr_logfile=/var/log/app/bravo-1-err.log
|
||||||
user=app
|
user=app
|
||||||
directory=/tmp/app
|
directory=/tmp/app
|
||||||
environment=PORT=5100
|
environment=PORT="5100"
|
||||||
|
|
||||||
|
[group:app]
|
||||||
|
programs=app-alpha,app-bravo
|
||||||
|
|||||||
Reference in New Issue
Block a user